Skip to main content

AOP

官方文档Core Technologies (spring.io)

AOP 的术语

  1. 连接点 Joinpoint

    连接点描述的是程序执行的某个特定位置。如一个类的初始化前、初始化后,或者类的某个方法调用前、调用后、方法抛出异常后等等。一个类或一段程序代码拥有一些具有边界性质的特定点,这些特定点就称为连接点。连接点用来定义在目标程序的哪里通过 AOP 加入新的逻辑。

    Spring 仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强。

    官方原文

    A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.

  2. 切入点Pointcut 切入点是一个连接点的过滤条件,AOP 通过切入点定位到特定的连接点。每个类都拥有多个连接点:例如 UserService 类中的所有方法实际上都是连接点。换言之,连接点相当于数据库中的记录,切点相当于查询条件。切入点和连接点不是一对一的关系,一个切入点对应多个连接点,切入点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

    官方原文

    A predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.

  3. 通知Advice

    切面在某个具体的连接点采取的行为或行动,称为通知。切面的核心逻辑代码都写在通知中,有人也称之为增强或者横切关注点。通知是切面功能的具体实现,通常是业务代码以外的需求,如日志、验证等。

    常用的接口

    • 前置通知:org.springframework.aop.MethodBeforeAdvice

    • 后置通知:org.springframework.aop.AfterReturningAdvice

    • 异常通知:org.springframework.aop.ThrowsAdvice 该接口没有要实现的方法,需要自定义一个 afterThrowing()方法。

    • 环绕通知:org.aopalliance.intercept.MethodInterceptor

官方原文 ​ A predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP, and Spring uses the AspectJ pointcut expression language by default.

  1. 切面Aspect

    切面是通知(Advice)和切点(Pointcut)的结合,通知和切点共同定义了切面的全部内容。因为通知定义的是切面的"要做什么"和"在何时做",而切点定义的是切面的"在何地做"。将两者结合在一起,就可以完美的。最后实现切面在何时,何地,做什么。

官方原文

​ A modularization of a concern that cuts across multiple classes. Transaction management is a good example of a crosscutting concern in enterprise Java applications. In Spring AOP, aspects are implemented by using regular classes (the schema-based approach) or regular classes annotated with the annotation (the @AspectJ style).@Aspect

切入点指示符

切入点指示符用来指示切入点表达式目的,在 Spring AOP 中目前只有执行方法这一个连接点,Spring AOP 支持的 AspectJ 切入点指示符如下:

execution:用于匹配方法执行的连接点;

within:用于匹配指定类型内的方法执行;

this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;

target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;

args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;

@within:用于匹配所以持有指定注解类型内的方法;

@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;

@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;

@annotation:用于匹配当前执行方法持有指定注解的方法;

bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;

reference pointcut:表示引用其他命名切入点,只有@ApectJ风格支持,Schema风格不支持。

AspectJ 切入点支持的切入点指示符还有: callgetsetpreinitializationstaticinitializationinitializationhandleradviceexecutionwithincodecflowcflowbelowif@this@withincode;但 Spring AOP 目前不支持这些指示符,使用这些指示符将抛出 IllegalArgumentException 异常。这些指示符 Spring AOP 可能会在以后进行扩展。

切入点使用示例

  1. execution:使用“execution(方法表达式)”匹配方法执行;
模式描述
public * *(..)任何公共方法的执行
_ com.mqb.IPointcutService._()com.mqb 包及所有子包下 IPointcutService 接口中的任何无参方法
_ com.mqb.._()com.mqb 包及所有子包下任何无参方法
_ com.mqb.._(..)com.mqb 包及所有子包下任何方法
_ 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_(..) throws IllegalArgumentException, ArrayIndexOutOfBoundsExceptioncn.javass 包及所有子包下 IPointcut 前缀类型和方法名以 test 为前缀的方法,且抛出 IllegalArgumentException 和 ArrayIndexOutOfBoundsException 异常
_ (cn.javass..IPointcutService+&& java.io.Serializable+)._(..)任何实现了 cn.javass 包及所有子包下 IPointcutService 接口和 java.io.Serializable 接口的类型的任何方法
@java.lang.Deprecated * *(..)任何持有@java.lang.Deprecated 注解的方法
(@cn.javass..Secure _) _(..)任何返回值类型持有@cn.javass..Secure 的方法
  1. within 使用“within(类型表达式)”匹配指定类型内的方法执行;

    模式描述
    within(cn.javass.._)cn.javass 包及子包下的任何方法执行
    within(cn.javass..IPointcutService+)cn.javass 包或所有子包下 IPointcutService 类型及子类型的任何方法
    within(@cn.javass..Secure _)持有 cn.javass..Secure 注解的任何类型的任何方法必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
  2. this:使用“this(类型全限定名)”匹配当前 AOP 代理对象类型的执行方法;注意是 AOP 代理对象的类型匹配,这样就可能包括引入接口方法也可以匹配;注意 this 中使用的表达式必须是类型全限定名,不支持通配符;

模式描述
this(cn.javass.spring.chapter6.service.IPointcutService)当前 AOP 对象实现了 IPointcutService 接口的任何方法
this(cn.javass.spring.chapter6.service.IIntroductionService)当前 AOP 对象实现了 IIntroductionService 接口的任何方法也可能是引入接口
  1. target :使用“target(类型全限定名)”匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意 target 中使用的表达式必须是类型全限定名,不支持通配符;
    模式描述
    target(cn.javass.spring.chapter6.service.IPointcutService)当前目标对象(非 AOP 对象)实现了 IPointcutService 接口的任何方法
    target(cn.javass.spring.chapter6.service.IIntroductionService)当前目标对象(非 AOP 对象) 实现了 IIntroductionService 接口的任何方法不可能是引入接口
  2. args:使用“args(参数类型列表)”匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args 属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
模式描述
args (java.io.Serializable,..)任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数的方法执行,args 指定的参数类型是在运行时动态匹配的

通知(advice)的类型

注解含义
@Before当切点在连接点开始执行前触发,参数可以写切入点的方法名,或者是 exexute(方法表达式)
@After当切点在连接点执行完成后触发,通常可以进行资源释放等等
@AfterReturning当方法执行返回后触发
@AfterThrowing当抛出异常时执行
@Around包围一个连接点的通知,类似 Web 中 Servlet 规范中的 Filter 的 doFilter 方法。可以在方法的调用前后完成自定义的行为, 也可以选择不执行。

image-20220710161341287

使用注解进行切面编程

业务逻辑代码

假设有一下业务逻辑代码,需要添加功能

package com.mqb.aop.application.service.impl;

import org.springframework.stereotype.Service;

/**
* @author qingbomy
* @date 2022/7/10 15:18
*/
@Service
public class UserService {
//模拟业务逻辑代码
public void query() {
System.out.println("执行业务逻辑代码");

}

public void insert() {

int a = 1 / 0;
}
public int returnTest(){
System.out.println("执行returnTest方法");
return 445;
}

}

启用 aop 代理

在主程序添加@EnableAspectJAutoProxy注解

定义切面

package com.mqb.aop.application.service.impl;
import org.aspectj.lang.annotation.Aspect;
/**
* @author qingbomy
* @date 2022/7/10 15:14
*/
@Aspect
@Component
public class NotVeryUsefulAspect {

}

定义切入点

package com.mqb.aop.application.service.impl;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

/**
* @author qingbomy
* @date 2022/7/10 15:14
*/
@Aspect
@Component
public class NotVeryUsefulAspect {
@Pointcut("execution(* com.mqb.aop.application.service..*(..))")
public void anyOldTransfer(){
}
}

定义通知

package com.mqb.aop.application.service.impl;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
* @author qingbomy
* @date 2022/7/10 15:14
*/
@Aspect
@Component
public class NotVeryUsefulAspect {

@Pointcut("execution(* com.mqb.aop.application.service.impl..*(..))")
public void anyOldTransfer() {
}

@Before("anyOldTransfer()")
public void advice() {
System.err.println("AOP");
}

@AfterThrowing("execution(* com.mqb.aop.application.service.impl.UserService.insert())")
public void adviceExcept() {
System.err.println("执行adviceExcept通知");
}

@AfterReturning("anyOldTransfer()")
public void adviceReturn(){
System.err.println("执行adviceReturn通知");
}
@After("anyOldTransfer()")
public void adviceAfter(){
System.err.println("执行adviceAfter通知");
}
@Around("anyOldTransfer()")
public Object adviceAround(ProceedingJoinPoint pjp) throws Throwable {
Integer returnValue = (Integer) pjp.proceed();
System.err.printf("执行adviceAround通知,方法返回值为%d\n",returnValue);
return returnValue;
}
}

测试 AOP

package com.mqb;

import com.mqb.aop.application.service.impl.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class MqbApplicationTests {

@Autowired
UserService userService;
@Test
void contextLoads() {
userService.query();
userService.returnTest();
userService.insert();

}

}

测试结果

image-20220711175152917

使用 xml 配置的 aop

<aop:config>
<!-- 声明切面 -->
<aop:aspect ref="切面类id">
<!-- 抽取切点表达式 -->
<aop:pointcut id="切点id" expression="切点表达式"/>
<!-- 设置增强 -->
<aop:增强方式 method="增强方法名" pointcut-ref="切点id"></aop:增强方式>
</aop:aspect>
</aop:config>

引入相关依赖

 <dependencies>
<!--导入spring的context坐标,context依赖aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- aspectj的织入(切点表达式需要用到该jar包) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
<!--spring整合junit-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

业务类

假设有下面的业务类,我们需要使用 aop 对其中的方法进行增强

public class AccountServiceImpl {
public void transfer() {
System.out.println("转账业务········");
}
}

通知类

需要使用该通知类对业务类中的匹配切入点表达式的方法进行增强

public class MyAdvice {
public void before(){
System.out.println("前置通知类");
}
}

配置文件 application.xml

  1. 在配置文件中配置 bean,将他们交由 spring 管理
<?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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="accountService" class="com.mqb.aop.application.service.impl.AccountServiceImpl"/>
<bean id="myAdvice" class="com.mqb.aop.application.service.impl.MyAdvice"/>

<aop:config>
<!--引入通知类-->
<aop:aspect ref="myAdvice">
<!--定义切点-->
<aop:pointcut id="customPointcut"
expression="execution(* com.mqb.aop.application.service.impl.AccountServiceImpl..*(..))"/>
<!--配置目标类的transfer方法执行时,使用通知类的before方法进行前置增强-->
<aop:before method="before" pointcut-ref="customPointcut"/>
</aop:aspect>
</aop:config>
</beans>

测试基于 xml 配置的 AOP

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application.xml")
public class Mytest {
@Autowired
private AccountServiceImpl accountService;
@Test
public void testTransfer(){
accountService.transfer();
}
}

测试结果

image-20220713205539174

总结

个人对 AOP 的理解

经过一段时间的学习总结,基本了解了 AOP 的使用场景、AOP 其实是一个“横切”技术。它可以在剖解封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,AOP 可以在不更改业务代码的同时,对指定的业务方法,或者一类匹配切入点的业务方法进行增强。利用这个特性,我们可以使用 AOP 对方法进行一个增强,列出如下几点

  1. 日志功能(在执行前进行日志处理)
  2. 事务处理(执行方法前,开启事务,执行完成后关闭事务,出现异常后回滚事务)
  3. 关闭资源(对于未释放的资源,可以使用 After (Finally) Advice 对其进行关闭,防止内存泄漏)
  4. 权限检查(在执行方法前,判断是否具有权限)

实现 AOP 的技术

两种,一种是动态代理技术,对目标对象进行代理,以此更改目标对象的行为。二是采用静态织入的方式,通过特定的语法创造切面切入,从而使编译器在编译期间就织入相关的切面。

Spring AOPAspectJ区别

  1. Spring AOP属于运行时的增强,而AspectJ属于编译时的增强。SpringAOP 是基于代理(Proxying),而AspectJ是基于字节码操作。

  2. 如果切面比较少,两者差不多,如果切面太多,最好使用AspectJ,它比Spring AOP快很多。

  3. 在性能上,由于Spring AOP是基于动态代理来实现的,AspectJ属于静态织入在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得Spring AOP的性能不如AspectJ的那么好

通知执行顺序

(1)没有异常情况下的执行顺序:

around before advice ->before advice->target method 执行->around after advice->after advice->afterReturning

(2)有异常情况下的执行顺序:

around before advice->before advice->target method 执行->around after advice->after advice->afterThrowing advice

/*
* #{copyright}#
*/
import org.springframework.aop.framework.AopContext;

/**
* 获取代理对象 self.
*
* @param <T>
* 接口类型
*/
public interface ProxySelf<T> {
/**
* 取得当前对象的代理.
*
* @return 代理对象,如果未被代理,则抛出 IllegalStateException
*/
@SuppressWarnings("unchecked")
default T self() {
return (T) AopContext.currentProxy();
}
}