Spring AOP使用

102 阅读7分钟

Spring AOP使用

  • AOP(面向切面编程),作为Spring框架中一个重大的特性,AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果,对于我们开发中最常见的可能就是日志记录,事务处理,异常处理等等。
  • AOP分为两种不同类型:静态AOP和动态AOP

静态AOP的意思就是:在编译期进行织入,就是说对切面进行的任何修改,都要进行重新编译程序。AspectJ就是一个典型的例子

  • 这个织入就是一个基本概念,通白来说就是把代码插入到执行的地方。上边这个例子中就是在Hello前后多打印两句,这个过程的思想就是织入。

动态AOP:在代码执行过程中进行织入,它的切面代码不是编译进class文件中的。Spring AOP是一种动态AOP

基础使用

  • 创建切点

    public class PointDemo {
        public void hello(){
            // 切点方法
            System.out.println("hello");
        }
    }
    
  • 创建切面类

    public class MyInterceptor implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            // 前置操作
            System.out.println("i am ...");
            // 执行切点方法
            Object result = invocation.proceed();
            // 后置操作
            System.out.println("who are you?");
            return result;
        }
    }
    
  • 使用

     public void test() {
            PointDemo pointDemo = new PointDemo();
            ProxyFactory proxyFactory = new ProxyFactory();
            // 设置代理对象和织入通知
            proxyFactory.addAdvice(new MyInterceptor());
            proxyFactory.setTarget(pointDemo);
            // 获取代理对象
            PointDemo proxy = (PointDemo) proxyFactory.getProxy();
            proxy.hello();
        }
    
  • 输出image-20231024202230070

使用切入点创建切面

  • 基础使用中的方式会对整个类的所有方法都进行织入通知,如果我们只想对类的部分方法进行织入就需要使用切入点来精确控制,切入点就相当于定义规则去匹配方法,匹配成功的方法就进行织入通知。

  • 创建切入点需要实现PointCut类

    public interface Pointcut{
    	ClassFilter getClassFilter();
    	MethodMatcher getMethodMacher();
    }
    
    // getClassFilter()源码
    /**
    *	传入一个类给这个接口,如果这个类满足我们的要求,就返回true,这个类就不需要匹配规则
    */
    @FunctionalInterface
    public interface ClassFilter {
        boolean matches(Class<?> var1);
    
    // getMethodMacher()源码
    /**
    *	这个当然就是用来匹配方法了,
    *	有两种类型,动态和静态,这个由isRuntime()的返回值来决定,true就是动态,false就是静态。这个类型就是决定了这个切入点是动态的还是静态的
    *	
    */
    public interface MethodMatcher {
        MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
    	//用于静态匹配,就是和方法的参数无关
        boolean matches(Method var1, Class<?> var2);
    
        boolean isRuntime();
    	//用于动态匹配,也就是和方法的参数有关,因为参数是会变的
        boolean matches(Method var1, Class<?> var2, Object... var3);
    }
    
    • 切点类可以分为动态和静态,但动态切点类每次进行匹配都要消耗额外的资源,所以尽量使用静态,可以提升性能。

    • 切点类的实现类

      实现类作用
      AnnotationMatchingPointcut在类或方法上找特定的注解,需要JDK5以上版本
      AspectJExpressionPointcut使用AspectJ织入器以AspectJ语法评估切入点表达式
      ComposablePointcut使用诸如union()和intersection()等操作组合两个或多个切入点
      ControlFlowPointcut是一种特殊的切入点,它们匹配另一个方法的控制流中的所有方法,即任何作为另一个方法的结果而直接或间接调用的方法
      JdkRegexpMethodPointcut对方法名使用正则表达式定义切入点,要JDK4以上
      NameMatchMethodPointcut根据方法名称进行匹配
      DynamicMethodMatcherPointcut这个类作为创建动态切入点的基类
      StaticMethodMatcherPointcut这个类作为创建静态态切入点的基类
  • 使用StaticMethodMatcherPointcut 自定义规则创建切入点

    • 创建类

      
      public class PointDemo {
          public void hello(){
              // 切点方法
              System.out.println("hello");
          }
          public void bye(){
              // 切点方法
              System.out.println("bye");
          }
      }
      
      
    • 创建切入点

      public class PiontCutDemo extends StaticMethodMatcherPointcut {
          @Override
          public boolean matches(Method method, Class<?> targetClass) {
              // 匹配bye()方法
              return "bye".equals(method.getName());
              // 下面这个匹配PointDemo类,只要匹配成功该类所有方法都会进行加强
              //return getClassFilter().matches(PointDemo.class);
          }
      }
      
      • 其中getClassFilter()父类已经实现过,我们不需要再实现
    • 通知类

      public class AdvisorDemo implements MethodInterceptor {
          @Override
          public Object invoke(MethodInvocation invocation) throws Throwable {
              System.out.println("nice to meet you");
              Object proceed = invocation.proceed();
              return proceed;
          }
      }
      
    • 使用

       public void test2(){
              PointDemo pointDemo = new PointDemo();
           	// 创建切入点
              PiontCutDemo piontCutDemo = new PiontCutDemo();
           	// 创建通知
              AdvisorDemo advisorDemo = new AdvisorDemo();
      		// 创建切面
              DefaultPointcutAdvisor advisor = new 		   DefaultPointcutAdvisor(piontCutDemo,advisorDemo);
      
              ProxyFactory proxyFactory = new ProxyFactory();
      		// 添加切面
              proxyFactory.addAdvisor(advisor);
           	// 添加织入对象
              proxyFactory.setTarget(pointDemo);
              PointDemo proxy = (PointDemo) proxyFactory.getProxy();
      
              proxy.hello();
      
              proxy.bye();
      
          }
      
    • 输出image-20231024202331915

  • 对于其他几个实现类我们不需要自己去继承再重写方法,spring已经为我们写好了他们的匹配规则,我们只需要根据需要直接在代码中进行使用。

SpringBoot中使用AOP

  • 前面对AOP的使用都是在我们手动进行织入的,而Spring容器可以帮我们进行管理,通过注解的形式告诉spring我们的切面即可。

  • 首先是我们需要导入的依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 主要是这个依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    

    相关术语

    • 切面(Aspect):一般是指被@Aspect修饰的类,代表着某一具体功能的AOP逻辑。

    • 切入点(Pointcut):选择哪些增强的方法,上述体现的是@pointcut注解和execution表达式

    • 通知(Advice)

      :对目标方法的增强

      • 环绕通知(@Around):内部执行连接点(方法),对其进行增强
      • 前置通知(@Before):在执行连接点前执行
      • 后置通知(@After):在执行连接点后执行
      • 返回通知(@AfterReturning):在连接点返回后执行
      • 异常通知(@AfterThrowing):在连接点爆出异常后执行
    • 连接点(JoinPoint):就是那些被切入点选中的方法

    简单使用

    
    @Aspect
    @Component
    public class DemoAOP {
    
        @Pointcut(value = "") //其中value可以写多种表达式定义切入点,后续详解
        public void pointCut() {
        }
    
        /**
         *前置通知,在切点执行之前执行的操作
         *
         */
        @Before("pointCut()")
        public void before(JoinPoint joinPoint) {
          	// 逻辑代码
        }
        
        /**
        * 其他通知类型,具体支持的类型见下文
        */
    }
    
    

    相关注解

  • @PointCut()

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    public @interface Pointcut {
        String value() default "";
    
        String argNames() default "";
    }
    
    
    • 可填入的表达式:

    • execution

      execution([可见性]返回类型[声明类型].方法名(参数)[异常]),其中[]内的是可选的,其他的还支持通配符的使用:

      • *: 匹配所有
      • … : 匹配多个包或多个参数
      • +: 表示类及其子类

      运算符:&&、||、!,通过运算符可以将多个表达式一起使用

    • within

      • 是用来指定类型的,指定类型中的所有方法将被拦截是用来指定类型的,指定类型中的所有方法将被拦截
      • within(com.demo.service.impl.UserServiceImpl) 匹配UserServiceImpl类对应对象的所有方法调用,并且只能是UserServiceImpl对象,不能是它的子对象
      • within(com.demo…*)匹配com.demo包及其子包下面的所有类的所有方法的外部调用
    • this

      • SpringAOP是基于代理的,this就代表代理对象,语法是this(type),当生成的代理对象可以转化为type指定的类型时表示匹配。
      • this(com.demo.service.IUserService)匹配生成的代理对象是IUserService类型的所有方法的外部调用
    • target

      • SpringAOP是基于代理的,target表示被代理的目标对象,当被代理的目标对象可以转换为指定的类型时则表示匹配。
      • target(com.demo.service.IUserService) 匹配所有被代理的目标对象能够转化成IuserService类型的所有方法的外部调用。
    • args

      • args用来匹配方法参数
      • args() 匹配不带参数的方法
      • args(java.lang.String) 匹配方法参数是String类型的
      • args(…) 带任意参数的方法
      • args(java.lang.String,…) 匹配第一个参数是String类型的,其他参数任意。最后一个参数是String的同理。
  • @within 和 @target

    • 带有相应标注的所有类的任意方法,比如@Transactional

    • @within(org.springframework.transaction.annotation.Transactional)
      @target(org.springframework.transaction.annotation.Transactional)
      
      
  • @annotation

    • 带有相应标注的任意方法,比如@Transactional或其他自定义注解

    • @annotation(org.springframework.transaction.annotation.Transactional)
      
      
  • @args

    参数带有相应标注的任意方法
    

使用总结

  • 通过@Aspect标志切面类并用@Component将切面类注入容器中,再通过@PointCut()等注解制定需要匹配的类或者方法,最后用@Around等注解标志通知类型。