深入了解Spring(三) - AOP

在学习AOP前可以先了解一下Java动态代理

https://www.cnblogs.com/binwenhome/p/13025480.html

AOP概述

  • 简介
  1. AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 对OOP(面向对象编程)的补充.
    • 面向对象: 纵向继承机制, 在该类中写代码.
    • 面向切面: 横向抽取机制, 把某些代码抽取到另外一个类中, 再作用到该类上.
  2. AOP编程操作的主要对象主要是切面, 而切面用于模块化横切关注点(公共功能)
    • 来存储公共功能的类就叫切面.
  3. AOP的好处
    • 每个事务逻辑位于一个位置, 代码不分散, 便于维护和升级.
    • 业务模块更简洁, 只包含核心代码.
  • AOP术语
  1. 横切关注点
    • 从每个方法中抽取出来的同一类非核心业务.
  2. 切面(Aspect)
    • 封装横切关注点信息的类, 每个关注点体现为一个通知方法.
  3. 通知(Advice)
    • 切面必须要完成的各个具体工作.
  4. 目标(Target)
    • 被通知的对象
  5. 代理(Proxy)
    • 向目标对象应用通知后创建的代理对象.
  6. 连接点(JoinPoint)
    • 横切关注点在程序代码中的具体体现, 对应程序执行的某个特定位置. 例如: 类某个方法调用前、调用后、方法捕获到异常后等
    • 在应用程序中可以使用横纵两个坐标来定位一个具体的连接点.
  7. 切入点(pointcut)
    • 要把切面作用到目标对象的一种方式, 即为使用切面的条件, 是个表达式.
    • AOP可以通过切入点定位到特定的连接点. 切点通过org.springframework.aop.Pointcut接口进行描述, 它使用类和方法作为连接点的查询条件. 
  8. 图解

  • AspectJ(切面)
  1. 简介
    • 是Java社区里最完整, 最流行的AOP框架.
    • Spring2.0以上, 可用基于AspectJ注解或XML配置的AOP.
  2. 在Spring中启用AspectJ注解支持.
    • 导入依赖
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-aop</artifactId>
                  <version>${spring.version}</version>
              </dependency>
      
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-aspects</artifactId>
                  <version>${spring.version}</version>
              </dependency>
    • 在配置文件中开启AspectJ的自动代理功能.
      <aop:aspectj-autoproxy />
    • 在Spring5中, 默认使用AspectJ动态代理, 所以不用加相关的jar了.
  • 用AspectJ注解声明切面
  1. 要在Spring中声明AspectJ切面, 只需要在IOC容器中将切面声明为bean实例.
  2. 当在SpringIOC容器中初始化AspectJ切面之后, SpringIOC容器就会为那些与AspectJ切面相匹配的bean创建代理.
  3. 在AspectJ注解中, 切面只是一个带有@Aspect注解的Java类.
  4. 通知是标注有某种注解的简单的Java方法.
  5. AspectJ支持5种类型的通知注解
    • @Before: 前置通知, 在方法执行前执行.
    • @After: 后置通知, 在方法执行之后执行.
    • @AfterRunning: 返回通知, 在方法返回结果之后执行.
    • @AfterThrowing: 异常通知, 在方法抛出异常之后执行.
    • @Around: 环绕通知, 围绕着方法执行.
  • 示例代码
  1. 注意: AOP也是基于IOC实现的, 所以需要IOC完成自动装配工作, 故仍需加入@Component注解.
  2. 使用配置类
    • 在配置类中加入@EnableAspecctJAutoProxy, 开启aspectj的自动代理功能.
      @EnableAspectJAutoProxy
      @Configuration
      @ComponentScan({"test"})
      public class MainConfigOfAOP {
      
          //...
      }
  3. aop.xml
        <!-- 开启aspectj的自动代理功能 -->
        <aop:aspectj-autoproxy />
    
        <!-- 扫描 -->
        <context:component-scan base-package="aop" />
  4. 结构
    @Component
    public class MathImpl implements MathI {
    
        //各个可执行方法
        //横切关注点在方法中.
    }
    
    //切面里封装横切关注点 @Component @Aspect
    //标注当前类为切面 public class MyLoggerAspect { //在切面中写各种通知, 切入点表达式加到通知上. }

     

 

AOP细节

  • 切入点表达式
  1. 作用: 通过表达式的方式定位一个或多个具体的连接点.
  2. 语法细节
    •  execution(权限修饰符 返回类型 全类名 方法名 参数列表)
      @Before(value = "execution(public int aop.MathImpl.add(int, int))")
    • 几种特殊形式
      execution(* aop.Calculator.* (..))
      Calculator接口中的所有方法
      第一个"*"表示任意修饰符, 任意返回值.
      第二个"*"表示任意方法
      ".."匹配任意数量, 任意类型的参数.
      
      execution(public * aop.Calculator.*(..))
      Calculator接口的所有公有方法
      
      execution(public double aop.Calculator.*(..))
      Calculator接口的中返回值double类型的方法
      
      execution(public * aop.Calculator.*(double, ..))
      Calculator接口中的公有方法且第一个参数为double类型
      
      execution(public * Calculator.*(double, double))
      参数为double, double的方法
    • 还可以通过"&&", "||", "!"等操作符结合起来
      execution(* *.add(int, ..)) || execution(* *.sub(int, ..))
      
      !execution(* *.add(int, ..))
  • 当前连接点细节
  1. 切入点表达式通常都会是从宏观上定位一组方法, 和具体某个通知的注解结合起来就能够确定对应的连接点, 那么就一个具体的连接点而言, 我们可能会关心这个连接点的一些具体信息. 
  2. JoinPoint
  • 通知
  1. 概述
    • 通知: 在具体的连接点上要执行的操作.
    • 一个切面可以包括多个通知.
    • 通知所使用的注解的值往往是切入点表达式.
  2. 前置通知
    • 方法执行前执行的通知.
    • 使用@Before注解
          /**
           * 切面里定义了通知, 而通知要想作用于连接点, 需要通过切入点表达式(只能定位到要作用的方法)
           *  而写的各种通知代表要作用的位置.
           */
          @Before(value = "execution(* aop.MathImpl.*(..))")
          public void beforeMeethod(JoinPoint joinPoint) {
              Object[] args = joinPoint.getArgs(); //获取方法参数
              String name = joinPoint.getSignature().getName(); //获取方法名
              System.out.println("method: " + name + ", arguments: " + Arrays.toString(args));
          }
  3. 后置通知
    • 连接点完成之后执行的, 作用于方法的finally语句块, 即有无异常都会执行.
    • 使用@After注解
          @After(value = "execution(* aop.MathImpl.*(..))")
          public void afterMethod() {
              System.out.println("后置通知");
          }
  4. 返回通知
    • 如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知.
    • 使用@AfterReturning注解, 在返回通知中访问连接点的返回值
      • 在返回通知中, 只要将returning属性添加到@AfterReturning注解中, 就可以访问连接点的返回值. 该属性的值即为用来传入返回值的参数名称.
      • 必须在通知方法的签名中添加一个同名参数. 在运行时Spring AOP会通过这个参数传递返回值.
            /**
             * @AfterReturning: 将方法标注为返回通知, 作用于方法执行之后.
             * 可通过returning设置接受方法返回值的变量名.
             * 要想在方法中使用, 必须在方法的形参中设置和变量名相同的参数名的参数, 类型为Object
             */
            @AfterReturning(value = "execution(* aop.MathImpl.*(..))", returning = "result")
            public void afterReturning(JoinPoint joinPoint, Object result) {
                String name = joinPoint.getSignature().getName();
                System.out.println("method: " + name + ", result: " + result);
            }
  5. 异常通知
    • 只在连接点抛出异常时才执行异常通知.
    • 将throwing属性添加到@AfterThrowing注解中, 才可以访问连接点抛出的异常. Throwable是所有错误和异常类的顶级父类, 所以在异常通知方法可以捕获到任何错误和异常.
    • 如果只对某种特殊的异常类型感兴趣, 可以将参数声明为其他异常的参数类
      型. 然后通知就只在抛出这个类型及其子类的异常时才被执行.
          /**
           * @AfterThrowing: 将方法标注为异常通知.
           * 异常通知: 当方法抛出异常时作用.
           * 通过throwing设置接受方法返回的异常信息.
           * 在参数列表中可通过具体的异常类型, 来对指定的一场进行操作
           */
          @AfterThrowing(value = "execution(* aop.MathImpl.*(..))", throwing = "e")
          public void afterThrowing(Exception e) {
              System.out.println("有异常了, message: " + e.getMessage());
          }
  6. 环绕通知
    • 环绕通知是所有通知类型中功能最为强大的, 能够全面地控制连接点,甚至可以控制是否执行连接点.
    • 对于环绕通知来说, 连接点的参数类型必须是ProceedingJoinPoint. 它是JoinPoint的子接口, 允许控制何时执行, 是否执行连接.
    • 在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法, 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.
    • 注意: 环绕通知的方法需要返回目标方法执行之后的结果, 即调用point.proceed(); 的返回值, 否则会出现空指针异常.
          @Around(value = "execution(* aop.MathImpl.*(..))")
          public Object arounMethod(ProceedingJoinPoint point) {
              Object result = null;
      
              try {
                  //前置通知
                  System.out.println("method: " + point.getSignature().getName() + ", arguments: " + Arrays.toString(point.getArgs()));
                  result = point.proceed();
                  
                  //返回通知
                  System.out.println("method: " + point.getSignature().getName() + ", result: " + result);
              } catch (Throwable throwable) {
                  //异常通知
                  System.out.println("有异常了, message: " + throwable.getMessage());
              } finally {
                  //后置通知
                  System.out.println("后置通知");
              }
              return result;
          }
  • 重用切入点定义
  1. 同一个切入点表达式可能在多个通知中重复出现, 可通过@Pointcut注解将切入点声明为简单的方法.
  2. 其他通知就可以通过方法名称引入该切入点.
        @Pointcut(value = "execution(* aop.MathImpl.*(..))")
        public void test() {
        }
    
        @Before(value = "test()")
        public void beforeMethod(JoinPoint joinPoint) {
            //...
        }
  • 指定切面的优先级
  1. 在同一个连接点上应用多个切面时, 除非明确指定, 否则优先级不确定.
  2. 切面的优先级可以通过@Order注解指定.
  3. @Order注解中值(非负数)越小, 优先级越高. 默认值为int的最大值.
    @Component
    @Order(0)
    @Aspect //标注当前类为切面
    public class MyLoggerAspect {
    
        //...
    }
  • 使用举例
    //核心类
    @Component
    public class MathImpl implements MathI { @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; } } //测试 public class TestBySpring { public static void main(String[] args) { //初始化容器 ClassPathXmlApplicationContext cac = new ClassPathXmlApplicationContext("conf/aop.xml");
    //注意, 这里不要自己创建对象, 要从容器中取, 否则无法启用AOP功能. MathI math
    = cac.getBean("mathImpl", MathI.class); System.out.println(math.getClass().getName()); int div = math.div(4, 1); System.out.println(div); cac.close(); } }

以XML方式配置切面

  1.  除了使用AspectJ注解声明切面, Spring也支持在bean配置文件中声明切面, 通过aop名称空间中的xml元素完成的.
  2. 配置细节
    • 在bean配置文件中, 所有的SpringAOP配置都必须定义在<aop:config>元素内部, 对于每个切面而言, 都要创建一个<aop:aspect>元素来为具体的切面实现引用后端bean实例.
    • <aop:aspect>中以ref属性标明具体的切面.
    • <aop:before>等, 以method属性标明方法, 以pointcut属性标明切入点表达式.
          <aop:config>
              <aop:aspect ref="myLoggerAspect">
                  <aop:before method="beforeMethod" pointcut="execution(* aop.MathImpl.*(..))" />
              </aop:aspect>
          </aop:config>
  3. 声明切入点
    • 切入点使用<aop:pointcut>元素声明.
    • 切入点可定义在<aop:aspect>或<aop:config>下.
      • 定义在<aop:aspect>下, 只对当前切面有效.
      • 定义在<aop:config>下, 对所有切面有效.
    • 通知元素需要使用<pointcut-ref>来引用切入.
    • 基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点.
          <aop:config>
              <aop:aspect ref="myLoggerAspect">
                  <aop:pointcut id="cut" expression="execution(* aop.MathImpl.*(..))"/>
                  <aop:before method="beforeMethod" pointcut-ref="cut" />
              </aop:aspect>
          </aop:config>
posted @ 2020-06-01 09:54  yellowstreak  阅读(195)  评论(0编辑  收藏  举报