Spring AOP的基本使用

2023年 7月 18日 76.7k 0

如何理解AOP

AOP的本质也是为了解耦,它是一种设计思想; 在理解时也应该简化理解。

AOP是什么

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程

AOP最早是AOP联盟的组织提出的,指定的一套规范,spring将AOP的思想引入框架之中,通过预编译方式和运行期间动态代理实现程序的统一维护的一种技术,

  • 先来看一个例子, 如何给如下UserServiceImpl中所有方法添加进入方法的日志,
/**
 * @author pdai
 */
public class UserServiceImpl implements IUserService {

    /**
     * find user list.
     *
     * @return user list
     */
    @Override
    public List findUserList() {
        System.out.println("execute method: findUserList");
        return Collections.singletonList(new User("pdai", 18));
    }

    /**
     * add user
     */
    @Override
    public void addUser() {
        System.out.println("execute method: addUser");
        // do something
    }

}

我们将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中!

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

AOP从横向解决代码重复的问题:取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)将业务逻辑和系统处理的代码进行解耦,如:关闭连接事务管理,操作日志的记录

AOP术语

首先让我们从一些重要的AOP概念和术语开始。这些术语不是Spring特有的。

  • 连接点(Jointpoint):所谓连接点是指那些被拦截到的点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为在哪里干;
  • 切入点(Pointcut): 选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合;
  • 通知(Advice):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知;通知包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么;
  • 方面/切面(Aspect):横切关注点的模块化,比如上边提到的日志组件。可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合;
  • 引入(inter-type declaration):也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象), 在AOP中表示为干什么(引入什么);
  • 目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干;
  • AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。在AOP中表示为怎么实现的一种典型方式;
  • 织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里,有以下几个点可以进行织入:
    • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
    • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。
    • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

对于织入这个概念,可以简单理解为aspect(切面)应用到目标函数(类)的过程。对于这个过程,一般分为动态织入和静态织入:

  • 动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术
  • ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
  • 通知类型:

    • 前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
    • 后置通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
    • 异常通知(After throwing advice):在方法抛出异常退出时执行的通知。
    • 最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
    • 环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

    环绕通知是最常用的通知类型。和AspectJ一样,Spring提供所有类型的通知,推荐使用尽可能简单的通知类型来实现需要的功能。例如,如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知,尽管环绕通知也能完成同样的事情。用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。比如,你不需要在JoinPoint上调用用于环绕通知的proceed()方法,就不会有调用的问题。

    一个demo

    xml配置方式

    package com.evan.aspect;
    
    
    import org.aspectj.lang.ProceedingJoinPoint;
    
    public class MyAspect {
        //定义该方法是一个前置通知
        public void before() {
            System.out.println("前置通知");
        }
    
        public void after() {
            System.out.println("后置通知");
        }
    
        public void afterReturning(Object result) {
            System.out.println("返回通知(After-returning)" + result);
        }
    
        public void afterThrowing(Exception e) {
            System.out.println("异常通知(After-throwing), 异常: " + e.getMessage());
        }
    
        public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("-----------------------");
            System.out.println("环绕通知: 进入方法");
            Object o = pjp.proceed();
            System.out.println(o);
            System.out.println("环绕通知: 退出方法");
            return o;
        }
    
    }
    
    package com.evan.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private String name;
        private Integer age;
    }
    
    package com.evan.service;
    
    public interface ArithmeticCalculator {
        Integer add(int i, int j);
    
        Integer sub(int i, int j);
    
        Integer mul(int i, int j);
    
        Integer div(int i, int j);
    }
    
    package com.evan.service.impl;
    
    import com.evan.service.ArithmeticCalculator;
    
    public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
        @Override
        public Integer add(int i, int j) {
            return i + j;
        }
    
        @Override
        public Integer sub(int i, int j) {
            return i - j;
        }
    
        @Override
        public Integer mul(int i, int j) {
            return i * j;
        }
    
        @Override
        public Integer div(int i, int j) {
            return i / j;
        }
    }
    
    
    
        
    
        
        
        
    
        
        
        
    
        
            
            
                
                
                
                
                
                
                
                
                
                
                
                
            
        
    
    
    package com.evan;
    
    import com.evan.service.ArithmeticCalculator;
    import org.junit.jupiter.api.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class AopTest {
    
        @Test
        public void test01() {
            ApplicationContext context =
                    new ClassPathXmlApplicationContext("spring.xml");
            ArithmeticCalculator arithmeticCalculator = context.getBean(ArithmeticCalculator.class);
            arithmeticCalculator.add(1, 2);
        }
    
        @Test
        public void test03() {
            ApplicationContext context =
                    new ClassPathXmlApplicationContext("spring.xml");
            ArithmeticCalculator arithmeticCalculator = context.getBean(ArithmeticCalculator.class);
            try {
                arithmeticCalculator.div(1, 0);
            } catch (Exception e) {
                //ignore
            }
        }
    }
    

    test01 测试结果

    -----------------------
    环绕通知: 进入方法
    前置通知
    3
    环绕通知: 退出方法
    返回通知(After-returning)3
    后置通知
    

    test02 测试结果

    -----------------------
    环绕通知: 进入方法
    前置通知
    异常通知(After-throwing), 异常: / by zero
    后置通知
    

    注解方式

    package com.evan.aspect;
    
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    
    @Aspect
    //相当于
    public class MyAspect {
        //定义该方法是一个前置通知
        @Before("execution(* com.evan.service.*.*(..))")
        public void before() {
            System.out.println("注解前置通知");
    
        }
    
        @AfterReturning(value = "pointcut()", returning = "returnVal")
        public void afterReturning(Object returnVal) {
            System.out.println("返回通知(After-returning)" + returnVal);
        }
    
        // 切点方法执行后运行,不管切点方法执行成功还是出现异常
        @After(value = "pointcut()")
        public void doAfter(JoinPoint joinPoint) {
            System.out.println("注解后置通知 After 方法执行 - Finish time: " + System.currentTimeMillis());
        }
    
        // 切点方法成功执行返回后运行
        @AfterReturning(value = "pointcut()", returning = "returnValue")
        public void doAfterReturning(JoinPoint joinPoint, Object returnValue) {
            System.out.println("AfterReturning 方法执行 - Returned value: " + returnValue);
        }
    
        // 切点方法成功执行返回后运行
        @AfterThrowing(value = "pointcut()", throwing = "throwing")
        public void doAfterThrowing(JoinPoint joinPoint, Object throwing) {
            System.out.println("异常通知(After-throwing), 方法执行 - throwing value: " + throwing);
        }
    
    
        @Around(value = "pointcut()")
        public Object doAround(ProceedingJoinPoint pjp) {
            System.out.println("-----------------------");
            System.out.println("环绕通知: 进入方法");
            Object o = null;
            try {
                o = pjp.proceed();
            } catch (Throwable e) {
                //throw new RuntimeException(e);
                System.out.println(e.getMessage());
            } finally {
                System.out.println("环绕通知: 退出方法");
            }
            return o;
        }
    
    
        @Pointcut(value = "execution(* com.evan.service.*.*(..))")
        public void pointcut() {
    
        }
    
    
    }
    
    package com.evan.config;
    
    import com.evan.aspect.MyAspect;
    import org.springframework.beans.factory.annotation.Configurable;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configurable
    @EnableAspectJAutoProxy
    @ComponentScan("com.evan")
    public class MyConfig {
    
        @Bean
        public MyAspect myAspect() {
            return new MyAspect();
        }
    }
    
    package com.evan.service.impl;
    
    import com.evan.service.ArithmeticCalculator;
    import org.springframework.stereotype.Service;
    
    @Service
    public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    
        @Override
        public int add(int i, int j) {
            int result = i + j;
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            int result = i - j;
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            int result = i * j;
            return result;
        }
    
        @Override
        public int div(int i, int j) {
            int result = i / j;
            return result;
        }
    }
    
    package com.evan;
    
    import com.evan.config.MyConfig;
    import com.evan.service.ArithmeticCalculator;
    import org.junit.jupiter.api.Test;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    public class AopTest {
    
    
        @Test
        public void test01() {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
            ArithmeticCalculator arithmeticCalculator = context.getBean(ArithmeticCalculator.class);
            arithmeticCalculator.add(1, 2);
        }
    
        @Test
        public void test03() {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
            ArithmeticCalculator arithmeticCalculator = context.getBean(ArithmeticCalculator.class);
            try {
                arithmeticCalculator.div(1, 0);
            } catch (Exception e) {
                //ignore
            }
        }
    }
    

    AOP使用小结

    AspectJ注解

    基于XML的声明式AspectJ存在一些不足,需要在Spring配置文件配置大量的代码信息,为了解决这个问题,Spring 使用了@AspectJ框架为AOP的实现提供了一套注解。

    注解名称 解释
    @Aspect 用来定义一个切面。
    @pointcut 用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。
    @Before 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。
    @AfterReturning 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。
    @Around 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。
    @After-Throwing 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。
    @After 用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。

    切入点指示符(PCD)的支持

    Spring AOP支持在切入点表达式中使用如下的AspectJ切入点指示符:

    • execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。
    • within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。
    • this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。
    • target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。
    • args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。
    • @target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。
    • @args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。
    • @within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。
    • @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。

    切入点(pointcut)的声明规则

    Spring AOP 用户可能会经常使用 execution切入点指示符。执行表达式的格式如下:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
    
    • ret-type-pattern 返回类型模式, name-pattern名字模式和param-pattern参数模式是必选的, 其它部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 你会使用的最频繁的返回类型模式是*,它代表了匹配任意的返回类型。
    • declaring-type-pattern, 一个全限定的类型名将只会匹配返回给定类型的方法。
    • name-pattern 名字模式匹配的是方法名。 你可以使用*通配符作为所有或者部分命名模式。
    • param-pattern 参数模式稍微有点复杂:()匹配了一个不接受任何参数的方法, 而(..)匹配了一个接受任意数量参数的方法(零或者更多)。 模式()匹配了一个接受一个任何类型的参数的方法。 模式(,String)匹配了一个接受两个参数的方法,第一个可以是任意类型, 第二个则必须是String类型。
    一、execution:使用“execution(方法表达式)”匹配方法执行;
    模式 描述
    public * *(..) 任何公共方法的执行
    * cn.javass..IPointcutService.*() cn.javass包及所有子包下IPointcutService接口中的任何无参方法
    * cn.javass...(..) cn.javass包及所有子包下任何类的任何方法
    * cn.javass..IPointcutService.() cn.javass包及所有子包下IPointcutService接口的任何只有一个参数方法
    * (!cn.javass..IPointcutService+).*(..) 非“cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法
    * cn.javass..IPointcutService+.*() cn.javass包及所有子包下IPointcutService接口及子类型的的任何无参方法
    * cn.javass..IPointcut*.test*(java.util.Date) cn.javass包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的;
    * cn.javass..IPointcut*.test*(..) throwsIllegalArgumentException, ArrayIndexOutOfBoundsException cn.javass包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException异常
    * (cn.javass..IPointcutService+&& java.io.Serializable+).*(..) 任何实现了cn.javass包及所有子包下IPointcutService接口和java.io.Serializable接口的类型的任何方法
    @java.lang.Deprecated * *(..) 任何持有@java.lang.Deprecated注解的方法
    @java.lang.Deprecated @cn.javass..Secure * *(..) 任何持有@java.lang.Deprecated和@cn.javass..Secure注解的方法
    @(java.lang.Deprecated cn.javass..Secure) * *(..) 任何持有@java.lang.Deprecated或@ cn.javass..Secure注解的方法
    (@cn.javass..Secure *) *(..) 任何返回值类型持有@cn.javass..Secure的方法
    * (@cn.javass..Secure ).(..) 任何定义方法的类型持有@cn.javass..Secure的方法
    * (@cn.javass..Secure () , @cn.javass..Secure (*)) 任何签名带有两个参数的方法,且这个两个参数都被@ Secure标记了,如public void test(@Secure String str1,@Secure String str1);
    * *((@ cn.javass..Secure ))或 *(@ cn.javass..Secure *) 任何带有一个参数的方法,且该参数类型持有@ cn.javass..Secure;如public void test(Model model);且Model类上持有@Secure注解
    * *(@cn.javass..Secure (@cn.javass..Secure *) ,@ cn.javass..Secure (@cn.javass..Secure *)) 任何带有两个参数的方法,且这两个参数都被@ cn.javass..Secure标记了;且这两个参数的类型上都持有@ cn.javass..Secure;
    * *(java.util.Map, ..) 任何带有一个java.util.Map参数的方法,且该参数类型是以为泛型参数;注意只匹配第一个参数为java.util.Map,不包括子类型;如public void test(HashMap map, String str);将不匹配,必须使用“* *(java.util.HashMap, ..)”进行匹配;而public void test(Map map, int i);也将不匹配,因为泛型参数不匹配
    * *(java.util.Collection) 任何带有一个参数(类型为java.util.Collection)的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass..Secure注解;如public void test(Collection collection);Model类型上持有@cn.javass..Secure
    * *(java.util.Set

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论