Spring学习(三)| Spring AOP

1. 什么是 AOP ?

  • AOP:面向切面编程,是对OOP(面向对象编程)的补充
  • AOP 的主要编程对象是切面(aspect),,是切面的横切关注点的模块化

2. 为什么需要使用 AOP ?

没有使用 AOP 之前,进行日志输出或者验证的之类需求开发时,会遇到以下问题
- 代码混乱:越来越多的非业务需求(日志和验证等)加入后, 原有的业务方法急剧膨胀. 每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点.
- 代码分散: 以日志需求为例, 只是为了满足这个单一需求, 就不得不在多个模块(方法)里多次重复相同的日志代码. 如果日志需求发生变化, 必须修改所有模块.

AOP 优势:

  • 每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级
  • 业务模块更简洁, 只包含核心业务代码.

3. AOP 术语

  • 切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
  • 通知(Advice): 切面必须要完成的工作
  • 目标(Target): 被通知的对象
  • 代理(Proxy): 向目标对象应用通知之后创建的对象
  • 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置
  • 切点(pointcut):每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

4. 在 Spring 中使用 Aspect 注解方式进行切面编程

4.1 在 Spring 中启用 AspectJ 注解支持

  • 要在 Spring 应用中使用 AspectJ 注解, 必须在 classpath 下包含 AspectJ 类库

    spring-aop-4.3.9.RELEASE.jar
    spring-aspects-4.3.9.RELEASE.jar
    com.springsource.org.aopalliance-1.0.0.jar
    com.springsource.org.aspectj.weaver_1.6.10.release.jar

  • 使用 aop 命名空间

  • 要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素 <aop:aspectj-autoproxy>

    <!-- 使 Aspject 注解起作用:自动为匹配的类生成代理对象 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
  • 当 Spring IOC 容器侦测到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理

4.2 用 AspectJ 注解声明切面

  • 把横切关注点的代码抽象到切面的类中

    • 切面首先是一个 IOC容器 中的 Bean, 即给切面类加入@Component注解
    • 切面还需要加入@Aspect注解
  • 在类中声明各种通知:
    通知是标注有某种注解的简单的 Java 方法(声明一个方法,在方法前加入如**解)

    注释 作用
    @Before 前置通知, 在方法执行之前执行
    @After 后置通知, 在方法执行之后执行(无论是否发生异常)
    @AfterRunning 返回通知, 在方法返回结果之后执行
    @AfterThrowing 异常通知, 在方法抛出异常之后
    @Around 环绕通知, 围绕着方法执行
  • 利用方法签名编写 AspectJ 切入点表达式@Before("execution(int com.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))")

  • 可以在通知方法中声明一个类型为 JoinPoint 的参数. 然后就能访问链接细节. 如方法名称和参数值.

/** * 日志切面类 * */
//把这个类声明为一个切面,需要把该类放入到 IOC容器中,在声明为一个切面
@Aspect
@Component
public class LoggingAspect {
	//声明该方法是一个前置通知:在目标方法开始之前执行
	@Before("execution(* com.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))")
	public void beforemethod(JoinPoint joinPoint) {
		String methodName = joinPoint.getSignature().getName();
		List<Object> args = Arrays.asList(joinPoint.getArgs());
		System.out.println("The method " + methodName + " begins with" + args);
	}
}

4.3 通知详解

  • 后置通知:后置通知是在连接点完成之后执行的(无论是否发生异常)

    • 后置通知因为可能出现异常,所以不能访问到方法的返回值的
  • 返回通知: 如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知

    • 返回通知是可以访问到方法的返回值的
    • 在返回通知中, 只要将 returning 属性添加到 @AfterReturning 注解中, 就可以访问连接点的返回值. 该属性的值即为用来传入返回值的参数名称.
    • 必须在通知方法的签名中添加一个同名参数. 在运行时, Spring AOP 会通过这个参数传递返回值.
    • 原始的切点表达式需要出现在 pointcut 属性中
  • 异常通知:只在连接点抛出异常时才执行异常通知,可以访问到方法出现的异常

    • 将 throwing 属性添加到 @AfterThrowing 注解中, 也可以访问连接点抛出的异常. Throwable 是所有错误和异常类的超类. 所以在异常通知方法可以捕获到任何错误和异常.
    • 如果只对某种特殊的异常类型感兴趣, 可以将参数声明为其他异常的参数类型. 然后通知就只在抛出这个类型及其子类的异常时才被执行.
  • 环绕通知:

5. 指定切面的优先级

  • 在同一个连接点上应用不止一个切面时, 除非明确指定, 否则它们的优先级是不确定的.
  • 切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定.
  • 实现 Ordered 接口, getOrder() 方法的返回值越小, 优先级越高.
  • 若使用 @Order 注解, 序号出现在注解中
    @Order(0)
    @Aspect
    @Component
    public class LoggingAspect {}
    ``

6. 重用切入点表达式

  • 在编写 AspectJ 切面时, 可以直接在通知注解中书写切入点表达式. 但同一个切点表达式可能会在多个通知中重复出现.

  • 在 AspectJ 切面中, 可以通过 @Pointcut 注解将一个切入点声明成简单的方法. 切入点的方法体通常是空的

    /** * 定义一个方法,用于声明切入点表达式,一般的,该方法不再需要填入其他的代码 * */
    @Pointcut("execution(int com.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))")
    public void dJoinPointExp(){}
    
    //调用的方法如下
    @After("dJoinPointExp()")
        public void afterMethod() {
            System.out.println("Method is ending...");
        }

7. 基于 XML 配置文件的方式来配置 AOP

  • 使用 aop 命名空间
    Eg:
<!-- 配置需要被切入的 bean -->
<bean id="arithmeticCalculator" class="com.spring.aop.xml.ArithmeticCalculatorImpl"></bean>
	
<!-- 配置切面的 bean -->
<bean id="loggingAspect" class="com.spring.aop.impl.LoggingAspect"></bean>

<!-- 配置 AOP -->
<aop:config>
    <!-- 配置切点表达式 -->
    <aop:pointcut expression="execution(int com.spring.aop.xml.ArithmeticCalculatorImpl.add(int, int))" id="pointcut"/>
    
    <!-- 配置切面及通知 -->
    <aop:aspect ref="loggingAspect" order="2">
        <aop:before method="beforeMethod" pointcut-ref="pointcut"/>
    </aop:aspect>
    
</aop:config>