1. 初识 AOP
AOP 全称 Aspect Oriented Programming,意为面向切面编程。切面指一类特定的,可以被统一处理的问题,因此 AOP 可以理解为面向特定问题编程。Spring 提供的拦截器就是对 AOP 思想的一种实现。除了解决登录校验的问题,我们还可能遇到许多其他的,需要被统一处理的问题,这些问题就需要使用 Spring 对 AOP 的支持来实现。
因此,简单来说 AOP 就是一种将一类问题集中处理的思想。
Spring AOP 是 AOP 思想的具体实现,或者说 Spring 对 AOP 的支持。除此之外,AspectJ、CGLIB 等都是 AOP 思想的实现。
假设现在有一个需求,记录项目中所有业务方法的执行时间。我们需要在每一个业务方法的执行前后分别记录时间戳,这是很大的工作量,并且它们是重复的劳动。而如果使用 AOP 的思想,将这些工作提取出来统一处理,就能减少大部分重复工作。并且 AOP 可以在不对原有方法做改动的基础上,只进行功能增强,因此这也是一种低耦合的开发方式。
2. Spring AOP 的使用
- 在 pom 文件中添加相应依赖。
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 确定待增强的方法。 目标:分别统计下面三个方法的执行时间。
java
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/t1")
public Integer t1() {
log.info("执行 t1");
return 1;
}
@RequestMapping("/t2")
public Boolean t2() {
log.info("执行 t2");
return true;
}
@RequestMapping("/t3")
public String t3() {
log.info("执行 t3");
return "hello";
}
}
- 写切面,描述当前遇到的统一问题,增强原方法。
java
@Aspect // 标识这是一个切面类
@Component // 需要将该类交给 Spring 管理
@Slf4j
public class AspectDemo1 {
// Around 环绕通知,表示通知可以在方法执行前,也可以在方法执行后
@Around("execution(* com.boilermaker.springaoplearning.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) {
// 前置通知(打印时间戳)
log.info("目标方法执行前...");
long begin = System.currentTimeMillis();
// 使用 Object 接收原始方法
Object result;
try {
result = pjp.proceed(); // 执行原始方法
} catch (Throwable e) {
log.info("目标方法抛出异常..."); // 异常后通知
throw new RuntimeException(e);
}
// 后置通知(打印时间戳,计算耗时)
log.info("目标方法执行后...");
long end = System.currentTimeMillis();
log.info(pjp.getSignature() + " 执行耗时:" + (end - begin) + " ms");
return result;
}
}
3. 代码详解
3.1 切点 Pointcut
告诉程序对哪些方法来进行功能增强。下面的代码就是切点表达式。
java
"execution(* com.boilermaker.springaoplearning.controller.*.*(..))"
execution( 访问修饰符 返回类型 包 - 类 - 方法 ( 方法参数 ) 异常 )
其中访问修饰符和异常是选填。
通配符表达:* 可以表示任意一个元素,如一层包、一个类、一个方法、一个返回类型、一个参数。.. 表示任意个连续的元素,如任意层包、任意个参数。
例:


我们也可以基于 @annotation 注解写针对多个无规则方法的切点表达式。比如 Controller1 中的 t1 方法和 Controller2 中的 t2 方法,这两个方法之间没有什么直接关联。此时,我们需要自定义注解,再使用 @annotation 表达切点,再在待增强方法上添加注解。
- 创建注解类,声明注解。
java
@Target(ElementType.METHOD) // 描述该注解可以被添加在什么地方
@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期
public @interface TimeMonitor {
}
- 编写切面类,和刚刚的代码基本一致,只是需要使用 @annotation 注解定义切点。下面的切点表达式意为只在加 TimeMonitor 注解的方法上做增强。
java
@Around("@annotation(com.boilermaker.springaoplearning.aspect.TimeMonitor)")
- 为目标方法添加 @TimeMonitor 注解。在这里我为 t1 加该注解。
分别请求 t1 和 t2,发现 t1 被增强,t2 无效果,符合预期。

对于相同的切点表达式,我们不需要写很多遍,可以使用 @Pointcut 注解将公共的切点表达式提取出来。
java
@Pointcut("execution(* com.boilermaker.springaoplearning.controller.*.*(..))")
public void controllerPointcut() {
}
...
// 后续使用时直接引用即可
@Around("controllerPointcut()")
3.2 切面 Aspect
使用切面来描述整个 AOP 程序,包括它针对于哪些方法,在什么时候执行什么样的操作。
切面所在的类,称为切面类,需要添加 @Aspect 注解。
当我们对同一个目标方法,应用多个切面类时,应使用 @Order 注解控制多个切面的执行顺序。其优先级遵循下图所示规则。

3.3 通知 Advice
通知就是我们具体要增强的那部分功能,也就是提取出的统一问题。
除了 @Around 环绕通知外,Spring 也单独提供了 @Before 前置通知、@After 后置通知、@AfterReturning 返回后通知和 @AfterThrowing 异常后通知。
其中 @After 和 @AfterReturning 的区别仅仅在于目标方法是否成功执行。如果目标方法执行成功,它们都可以正常增强,如果目标方法执行失败,@AfterReturning 将会失效,@After 依然可以正常增强。
@Around 更加自由,因为它在代码中调用 pjp.proceed() 来自由控制目标方法执行时间。如果目标方法执行失败,那么写在 pjp.proceed() 后的逻辑不会执行。所以 @Around 和 @After 这两个通知类型就可以应对全部情况了。
3.4 连接点 Join Point
连接点是满足切点表达式的元素,相当于切点的真子集。
比如在下面的切点表达式中,controller 包下全部类的全部方法都是连接点。
java
"execution(* com.boilermaker.springaoplearning.controller.*.*(..))"
4. Spring AOP 原理
4.1 代理模式
代理模式下,代理对象与真实对象实现相同的接口,客户端无需直接操作真实对象,而是通过代理对象间接访问,从而在访问过程中附加额外功能(如日志记录、权限校验、延迟加载等)或限制访问。代理模式的本质是功能增强。
代理模式中的角色:
Subject 业务接口:定义代理和真实对象的共同接口,客户端通过该接口访问对象。
RealSubject 业务具体实现类:实现接口的具体业务逻辑,是代理的目标对象。
Proxy 代理类:实现抽象接口,持有目标对象的引用,在调用目标对象前后添加额外操作。
代理模式又分为静态代理和动态代理。我们很早就知道,Java 程序的运行需要经历,.java 文件的编写,编译器将其编译为 .class 文件,JVM 读取 .class 文件并执行这几个过程。静态代理就是指,代理类是由开发者自行编写,其对应的 .class 文件在运行前存在。相对的,动态代理的代理类由 JVM 来实现,在程序运行时根据需要动态生成。
4.1.1 静态代理
java
-----> Subject 抽象接口
public interface UserService {
String queryUser(String id);
}
-----> RealSubject 实现用户服务的具体逻辑
public class RealUserService implements UserService {
@Override
public String queryUser(String id) {
return "用户信息:id=" + id; // 实际业务逻辑
}
}
-----> Proxy 代理持有目标对象引用,添加日志功能
public class UserServiceProxy implements UserService {
private UserService realUserService; // 持有真实对象
public UserServiceProxy(UserService realUserService) {
this.realUserService = realUserService;
}
@Override
public String queryUser(String id) {
// 调用前:添加日志
System.out.println("日志:开始查询用户,id=" + id);
// 调用真实对象的方法
String result = realUserService.queryUser(id);
// 调用后:添加日志
System.out.println("日志:查询用户结束,结果=" + result);
return result;
}
}
java
客户端通过代理间接访问目标对象
public class Client {
public static void main(String[] args) {
UserService realService = new RealUserService();
UserService proxy = new UserServiceProxy(realService); // 创建代理
proxy.queryUser("1001"); // 客户端调用代理
}
}
diff
输出:
日志:开始查询用户,id=1001
用户信息:id=1001
日志:查询用户结束,结果=用户信息:id=1001
静态代理不够灵活。假设 Subject 中增加了许多接口,那么 Proxy 也需要相应地进行增加,但是实际上我们对这些方法的增强功能都是一样的,这就是在做重复的工作。我们使用代理就是为了避免重复,但是静态代理实际上只是将代码换了个地方,并没有减少任何工作量。因此静态代理几乎用不上,它只是帮我们更好地理解代理思想。
4.1.2 动态代理
对于动态代理来说,创建代理对象的工作被推迟到程序运行时,由 JVM 根据我们的需要来自行实现。
我们知道,JVM 执行字节码前有一个关键步骤,就是 JVM 通过类加载,将 .class 文件加载到内存,生成 java.lang.Class 对象。通过 Class 对象可以获取类的全部信息,这是 Java 反射机制的基础。Java 的反射 API 本质就是对 Class 中信息的读取和操作,发生在运行时,因此与反射有关的操作我们往往加以动态二字。
动态代理也不例外,它的核心就是反射机制。动态代理在运行时生成的代理类,本质就是动态生成的字节码。
具体原理如下:
先给出业务接口和其具体实现类。
java
// -----> Subject <-----
public interface UserService {
void queryUser(String id);
void deleteUser(String id);
}
// -----> RealSubject <-----
public class RealUserService implements UserService {
@Override
public void queryUser(String id) {
System.out.println("查询中, id = " + id);
}
@Override
public void deleteUser(String id) {
System.out.println("删除中, id = " + id);
}
}
在拦截器中定义增强逻辑,JDK 动态代理规定拦截器需实现 InvocationHandler 接口。
java
public class UserInvocationHandler implements InvocationHandler {
private Object target;
public UserInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置通知: ...");
Object ret = method.invoke(target, args);
System.out.println("后置通知: ...");
return ret;
}
}
现在的需求是增强 queryUser 和 deleteUser 方法。
使用动态代理,第一步,我们需要一个目标方法所在类的实例。它有两个作用,首先 Proxy 必须使用它的类加载器,并且它将作为参数传入拦截器。
java
UserService realUserService = new RealUserService();
接下来,调用 Proxy.newProxyInstance() 方法来创建代理。
java
UserService proxy = (UserService) Proxy.newProxyInstance(
// 类加载器
realUserService.getClass().getClassLoader(),
// 被代理类实现的接口
new Class[]{UserService.class},
// 指定拦截器
new UserInvocationHandler(realUserService)
);
代码的核心是方法的第二个参数,目标方法所在类实现的接口。这个参数告知 Proxy,需要生成一个实现 UserService 接口的代理类。它将在运行时动态生成实现该接口全部方法的字节码。实现逻辑类似下面的代码。
java
// proxy 伪代码
public class proxy extends Proxy implements UserService {
@Override
public void queryUser(String id) {
// Proxy 首先通过反射将方法封装在 Method 中
Method method = UserService.class.getMethod("queryUser", String.class);
// 调用传入的拦截器,进行增强操作
userInvocationHandler.invoke(this, method, new Object[]{id});
}
@Override
public void updateUser(String id) {
Method method = UserService.class.getMethod("updateUser", String.class);
userInvocationHandler.invoke(this, method, new Object[]{id});
}
}
对于拦截器中的 invoke 方法。第一个参数是代理对象本身,这个参数一般不会用到。第二个参数是 proxy 封装的方法对象,它用于 method.invoke(realUserService, args),即反射调用目标对象的真实方法。第三个参数是当前方法调用的参数列表,比如调用 queryUser("1010") 时,args 就是 new Object [] {"1010"}。
java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {}
拦截器中的 invoke 方法由自动生成的 proxy 调用,invoke 方法中依然存在一个 invoke 方法,这个 invoke 方法是连接代理与目标对象的关键,因为它是在通过反射调用 RealSubject 中的方法。
java
Object ret = method.invoke(target, args);
完整客户端代码如下。后续通过 proxy 调用方法,实际上调用的是 proxy 重写的方法。
java
public class Client {
public static void main(String[] args) {
UserService realUserService = new RealUserService();
UserService proxy = (UserService) Proxy.newProxyInstance(
realUserService.getClass().getClassLoader(),
new Class[]{UserService.class},
new UserInvocationHandler(realUserService)
);
proxy.queryUser("1010");
proxy.deleteUser("1010");
}
}