Spring 详解(二)——- AOP关键概念以及两种实现方式

  • 2019 年 10 月 6 日
  • 筆記

目录


1. AOP 关键词

  • target:目标类,需要被代理的类。例如:ArithmeticCalculator
  • Joinpoint(连接点):所谓连接点是指那些可能被拦截到的方法。例如:所有的方法
  • PointCut 切入点:已经被增强的连接点。例如:add()
  • advice:通知/增强,增强代码。例如:showRaram、showResult
  • Weaving(织入):是指把增强 advice 应用到目标对象 target 来创建新的代理对象proxy的过程.
  • proxy 代理类:通知+切入点
  • Aspect(切面)::是切入点 pointcut 和通知 advice 的结合

2. AOP 的作用

当我们为系统做参数验证,登录权限验证或者日志操作等,为了实现代码复用,我们可能把日志处理抽离成一个新的方法。但是这样我们仍然必须手动插入这些方法,这样的话模块之间高耦合,不利于后期的维护和功能的扩展,有了 AOP 我们可以将功能抽成一个切面,代码复用好,低耦合。

3. AOP 的通知类型

Spring 按照通知 Advice 在目标类方法的连接点位置,可以分为5类

  • 前置通知[Before advice]:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。
  • 正常返回通知[After returning advice]:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。
  • 异常返回通知[After throwing advice]:在连接点抛出异常后执行。
  • 返回通知[After (finally) advice]:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。
  • 环绕通知[Around advice]:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。 Spring 中使用五种通知

1. 前置通知 <aop:before method="" pointcut="" pointcut-ref=""/> method : 通知,及方法名 pointcut :切入点表达式,此表达式只能当前通知使用。 pointcut-ref : 切入点引用,可以与其他通知共享切入点。 通知方法格式:public void myBefore(JoinPoint joinPoint){ 参数1:org.aspectj.lang.JoinPoint 用于描述连接点(目标方法),获得目标方法名等 2. 后置通知 目标方法后执行,获得返回值 <aop:after-returning method="" pointcut-ref="" returning=""/> returning 通知方法第二个参数的名称 通知方法格式:public void myAfterReturning(JoinPoint joinPoint,Object result){ 参数1:连接点描述 参数2:类型Object,参数名 returning="result" 配置的 3. 异常通知 目标方法发生异常后 <aop:after-throwing method="testException" throwing="e" pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/> throwing 发生的异常 通知方法格式:public Object testRound(ProceedingJoinPoint pjp){ 参数1:ProceedingJoinPoint 返回值为 reslut

4. 基于 xml 的配置方式

xml 配置文件

<context:component-scan base-package="com.anqi">      <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>  </context:component-scan>  <!--1、 创建目标类 -->  <bean id="arithmeticCalculator" class="com.anqi.testAop.ArithmeticCalculatorImpl"></bean>  <!--2、创建切面类(通知)  -->  <bean id="logAspect" class="com.anqi.testAop.MyLogger"></bean>  <aop:config>      <aop:aspect ref="logAspect">          <!-- 切入点表达式 也可以在通知内部分别设置切入点表达式 -->          <aop:pointcut expression="execution(* com.anqi.testAop.*.*(..))" id="myPointCut"/>          <!-- 配置前置通知,注意 method 的值要和 对应切面的类方法名称相同 -->          <aop:before method="before" pointcut-ref="myPointCut" />          <aop:after method="after" pointcut-ref="myPointCut" />          <aop:after-returning method="testAfterReturn" returning="result" pointcut-ref="myPointCut"/>          <aop:after-throwing method="testException" throwing="e" pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/>          <!--<aop:around method="testRound"  pointcut-ref="myPointCut"  /> 最强大,但是一般不使用-->      </aop:aspect>  </aop:config>

目标类

public interface ArithmeticCalculator {      int add(int i, int j);      int sub(int i, int j);        int mul(int i, int j);      int div(int i, int j);  }    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;      }  }

切面类

import org.aspectj.lang.JoinPoint;  import org.aspectj.lang.ProceedingJoinPoint;  import java.util.Arrays;    /**   * 创建日志类   */  public class MyLogger {        public void before(JoinPoint joinPoint) {          System.out.println("前置通知 参数为["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");      }      public void after(JoinPoint joinPoint) {          System.out.println("后置通知 "+ joinPoint.getSignature().getName());      }        public void testException(JoinPoint joinPoint, Throwable e) {          System.out.println("抛出异常: "+ e.getMessage());      }        public void testAfterReturn(JoinPoint joinPoint, Object result) {          System.out.println("返回通知,返回值为 " + result);      }        public Object testRound(ProceedingJoinPoint pjp) {          Object result = null;          String methodName = pjp.getSignature().getName();          Object[] args = pjp.getArgs();          try {              //前置通知              System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));              //执行目标方法              result = pjp.proceed();              //返回通知              System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);            }catch(Throwable e) {              //异常通知              System.out.println("!!!异常通知 --> The Method"+methodName+" ends with"+ result);          }          //后置通知          System.out.println("!!!后置通知 --> The Method"+methodName+" ends"+ args);          return result;      }  }

测试

import org.springframework.context.ApplicationContext;  import org.springframework.context.support.ClassPathXmlApplicationContext;  public class Main {      public static void main(String[] args) {          ApplicationContext application = new ClassPathXmlApplicationContext("spring-context.xml");          ArithmeticCalculator a = application.getBean(ArithmeticCalculator.class);          int result = a.add(1,2);          System.out.println(result);          System.out.println(a.div(5,0));      }  }  /*      前置通知 参数为[1,2]      后置通知 add      返回通知,返回值为 3      3      前置通知 参数为[5,0]      后置通知 div      抛出异常: / by zero  */

5. 基于注解的配置方式

xml

<?xml version="1.0" encoding="UTF-8"?>  <beans xmlns="http://www.springframework.org/schema/beans"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xmlns:context="http://www.springframework.org/schema/context"         xmlns:aop="http://www.springframework.org/schema/aop"         xsi:schemaLocation="http://www.springframework.org/schema/beans         http://www.springframework.org/schema/beans/spring-beans.xsd         http://www.springframework.org/schema/context         http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">        <context:component-scan base-package="com.anqi">          <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>      </context:component-scan>      <!-- 使 AspectJ 注解起作用: 自动为匹配的类生成代理对象 -->      <aop:aspectj-autoproxy/>  </beans>

目标类

public interface ArithmeticCalculator {      int add(int i, int j);      int sub(int i, int j);        int mul(int i, int j);      int div(int i, int j);  }  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;      }  }

切面

import org.aspectj.lang.JoinPoint;  import org.aspectj.lang.ProceedingJoinPoint;  import org.aspectj.lang.annotation.*;  import org.springframework.stereotype.Component;  import java.util.Arrays;    /**   * 创建日志类   */  @Aspect  @Component  public class MyLogger {        @Before("execution(* com.anqi.testAop.*.*(..))")      public void before(JoinPoint joinPoint) {          System.out.println("前置通知 参数为["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");      }      @After("execution(* com.anqi.testAop.*.*(..))")      public void after(JoinPoint joinPoint) {          System.out.println("后置通知 "+ joinPoint.getSignature().getName());      }        @AfterThrowing(value="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))", throwing = "e")      public void testException(JoinPoint joinPoint, Throwable e) {          System.out.println("抛出异常: "+ e.getMessage());      }        @AfterReturning(value="execution(* com.anqi.testAop.*.*(..))", returning = "result")      public void testAfterReturn(JoinPoint joinPoint, Object result) {          System.out.println("返回通知,返回值为 " + result);      }        @Around("execution(* com.anqi.testAop.*.*(..))")      public Object testRound(ProceedingJoinPoint pjp) {          Object result = null;          String methodName = pjp.getSignature().getName();          Object[] args = pjp.getArgs();          try {              //前置通知              System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));              //执行目标方法              result = pjp.proceed();              //返回通知              System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);            }catch(Throwable e) {              //异常通知              System.out.println("!!!异常通知 --> The Method"+methodName+" ends with"+ result);          }          //后置通知          System.out.println("!!!后置通知 --> The Method"+methodName+" ends"+ args);          return result;      }  }

输出结果与第一种方式一致,这里就不再赘述了。

6. 切面的优先级

可以使用@Order来指定切面的优先级

//参数验证切面  @Order(1)  @Aspect  @Component  public class ValidateAspect {    @Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")  public void validateArgs(JoinPoint join) {      String methodName = join.getSignature().getName();      Object[] args = join.getArgs();      System.out.println("validate"+methodName+Arrays.asList(args));      }  }    //把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面  @Order(2)  @Aspect  @Component  public class LoggingAspect2 {    /**   * 声明该方法是一个前置通知: 在目标方法开始之前执行   * @param join   */  @Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")  public void beforeMehod(JoinPoint join) {      String methodName = join.getSignature().getName();      List<Object> args = Arrays.asList(join.getArgs());      System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);      }  }

7. 重用切点表达式

//把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面  @Order(2)  @Aspect  @Component  public class LoggingAspect {        /**       * 定义一个方法, 用于声明切入点表达式, 一般地, 该方法中再不需要填入其他代码       */      @Pointcut("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")      public void declareJointPointExpression() {}          /**       * 声明该方法是一个前置通知: 在目标方法开始之前执行       * @param join       */      @Before("declareJointPointExpression()")      public void beforeMehod(JoinPoint join) {          String methodName = join.getSignature().getName();          List<Object> args = Arrays.asList(join.getArgs());          System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);      }  }

8. 两种方式的比较(摘自 spring 官方文档)

如果您选择使用Spring AOP,则可以选择@AspectJ或XML样式。需要考虑各种权衡。

XML样式可能是现有Spring用户最熟悉的,并且由真正的POJO支持。当使用AOP作为配置企业服务的工具时,XML可能是一个不错的选择(一个好的测试是你是否认为切入点表达式是你可能想要独立改变的配置的一部分)。使用XML样式,从您的配置可以更清楚地了解系统中存在哪些方面。

XML风格有两个缺点。首先,它没有完全封装它在一个地方解决的要求的实现。DRY原则规定,系统中的任何知识都应该有单一,明确,权威的表示。使用XML样式时,有关如何实现需求的知识将分支到支持bean类的声明和配置文件中的XML。使用@AspectJ样式时,此信息封装在单个模块中:方面。其次,XML样式在它所表达的内容方面比@AspectJ样式稍微受限:仅支持“单例”方面实例化模型,并且不可能组合在XML中声明的命名切入点。例如,

@Pointcut("execution(* get*())")  public void propertyAccess() {}    @Pointcut("execution(org.xyz.Account+ *(..))")  public void operationReturningAnAccount() {}    @Pointcut("propertyAccess() && operationReturningAnAccount()")  public void accountPropertyAccess() {}

在XML样式中,您可以声明前两个切入点:

<aop:pointcut id="propertyAccess"          expression="execution(* get*())"/>    <aop:pointcut id="operationReturningAnAccount"          expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺点是您无法 accountPropertyAccess通过组合这些定义来定义切入点。

@AspectJ 样式支持额外的实例化模型和更丰富的切入点组合。它具有将方面保持为模块化单元的优点。它还具有以下优点:Spring AOP 和 AspectJ 都可以理解(并因此消耗)@AspectJ 方面。因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,则可以轻松迁移到基于 AspectJ 的方法。总而言之,只要您的方面不仅仅是简单的企业服务配置,Spring 团队更喜欢 @AspectJ 风格。