什么是AOP
Aspect Oriented Programming(面向切面编程)
-
什么是面向切面编程呢? 切⾯就是指某⼀类特定问题, 所以AOP也可以理解为面向特定方法编程.
-
什么是面向特定方法编程呢? 比如对于"登录校验", 就是⼀类特定问题. 登录校验拦截器, 就是对"登录校验"这类问题的统⼀处理. 所以, 拦截器也是AOP的⼀种应⽤. AOP是⼀种思想, 拦截器是AOP思想的⼀种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接⼝.
-
同样的, 统⼀数据返回格式和统⼀异常处理, 也是AOP思想的⼀种实现.
-
简单来说AOP是一种思想,是对某一类事情的集中处理,实现的方式有很多,有SpringAOP,AspectJ,CGLIB等
SpringAOP快速入门
我们通过一个经典的“方法耗时统计”案例来快速上手 Spring AOP。
引入AOP依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写切面代码
一个切面包含了我们要执行的操作(通知) 和指定在何处执行(切点)。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;@Aspect // 声明这是一个切面类
@Component // 将其作为Bean交由Spring容器管理
@Slf4j
public class TimeRecordAspect {// 1. 使用 @Pointcut 定义一个可重用的切点// 匹配 com.example.service 包及其子包下的所有类的所有方法@Pointcut("execution(* com.example.service..*.*(..))")public void serviceMethods() {}// 2. 定义通知(Advice),并引用上面的切点@Around("serviceMethods()")public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {// joinPoint 代表被拦截的目标方法String methodName = joinPoint.getSignature().toShortString();long startTime = System.currentTimeMillis();log.info("==> 开始执行 [{}], 参数: {}", methodName, joinPoint.getArgs());// 3. 调用 proceed() 执行原始的目标方法Object result = joinPoint.proceed();long endTime = System.currentTimeMillis();log.info("<== 执行 [{}] 结束, 耗时: {} ms, 返回值: {}", methodName, (endTime - startTime), result);return result;}
}
代码解析:
@Aspect
: 标志着这个类是一个切面。@Pointcut
: 用于声明一个可重用的切点表达式。这样,当多个通知需要应用在相同的切点时,我们就不需要重复书写冗长的execution
表达式了。@Around
: 环绕通知。这是功能最强大的通知类型,它能完全控制目标方法的执行,你可以在方法执行前后添加自定义逻辑,甚至可以决定是否执行目标方法。ProceedingJoinPoint
: 连接点对象,只在@Around
通知中使用。它代表了被拦截的目标方法,通过调用其proceed()
方法来执行原始方法。
[!INFO] SpringAOP只是使用了Aspect的注解
注解分为两个步骤:1. 声明 2. 实现
Spring中AOP的通知类型有以下几种:
@Around
: 环绕通知。在目标方法执行前后都可执行,可以控制目标方法的执行。@Before
: 前置通知。在目标方法执行前执行。@After
: 后置通知。在目标方法执行后执行,无论方法是正常返回还是抛出异常,它都会执行(类似于finally
块)。@AfterReturning
: 返回后通知。在目标方法成功执行并返回结果后执行,如果方法抛出异常则不会执行。@AfterThrowing
: 异常后通知。在目标方法抛出异常后执行。
@PointCut
如果有大量的方法,那么就会存在大量的切点表达式,此时可以使用@PointCut
把公共的切点表达式提取出来,需要时引入即可
@Aspect
@Component
@Slf4j
public class TimeRecordAspect { @Pointcut("execution(* com.doublez.springbook.controller.*.*(..))") private void pt(){} @Around("pt()") public Object timeRecordAspect(ProceedingJoinPoint joinPoint) throws Throwable { //... }@Around("pt()")public Object timeRecordAspect1(ProceedingJoinPoint joinPoint) throws Throwable {//...}@Around("pt()")public Object timeRecordAspect2(ProceedingJoinPoint joinPoint) throws Throwable {//...}@Around("pt()")public Object timeRecordAspect3(ProceedingJoinPoint joinPoint) throws Throwable {//...}
}
当切点定义使用private修饰时, 仅能在当前切⾯类中使用, 当其他切面类也要使用当前切点定义时, 就需要把private改为public. 引用方式为: 全限定类名.方法名()
切点表达式
常见的有两种:@execution
@annotation
execution
-
通配符
*
的用法:- 匹配任意字符,但仅匹配一个元素,如返回类型、包名、类名、方法名或方法参数。
*
在包名中表示任意包(一层包)。*
在类名中表示任意类。*
在返回值中表示任意返回值类型。*
在方法名中表示任意方法。*
在参数中表示一个任意类型的参数。
-
通配符
..
的用法:- 匹配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数。
..
配置包名时,标识此包及其所有子包。..
配置参数时,表示任意个任意类型的参数。
-
切点表达式示例:
execution(public String com.example.demo.controller.TestController.t1())
:匹配 TestController 下的 public 修饰,返回类型为 String,方法名为 t1,无参方法。execution(String com.example.demo.controller.TestController.t1())
:省略访问修饰符的情况。execution(* com.example.demo.controller.TestController.t1())
:匹配所有返回类型。execution(* com.example.demo.controller.TestController.*())
:匹配 TestController 下的所有无参方法。execution(* com.example.demo.controller.TestController.*(..))
:匹配 TestController 下的所有方法。execution(* com.example.demo.controller.*.*(..))
:匹配 controller 包下所有类的所有方法。execution(* com..TestController.*(..))
:匹配所有包下面的 TestController。execution(* com.example.demo...(..))
:匹配 com.example.demo 包下,子孙包下的所有类的所有方法。
AOP可以识别类的私有方法吗,可以的话推荐吗? ->here
@annotation
用于自定义类的实现:[[@annotation自定义注解实现|here]]
切面优先级@Order
- 数字越大,优先级越低(可以有负数)
@Aspect
@Component
@Order(1)
public class Demo {
}
@Aspect
@Component
@Order(3)
public class Demo2 {
}
AOP原理
Spring AOP 并没有在编译期修改你的代码,而是在运行时通过动态代理技术实现的。当你从 Spring 容器中获取一个 Bean 时,如果这个 Bean 需要被 AOP 增强,那么你拿到的其实是一个代理对象,而不是原始对象。所有对方法的调用都会先经过这个代理对象,由它来决定何时执行切面逻辑、何时执行原始方法。
[[代理模式]]
-
定义:为其他对象提供⼀种代理以控制对这个对象的访问. 它的作用就是通过提供⼀个代理类, 让我们在调用目标方法的时候, 不再是直接对目标方法进行调用, 而是通过代理类间接调用.
-
在某些情况下, ⼀个对象不适合或者不能直接引用另⼀个对象,而代理对象可以在客户端和目标对象之间起到中介的作用
代理模式的主要角色
- Subject: 业务接口类,可以是抽象类或者接口(不一定有)
- RealSubject:业务实现类,具体的业务执行,也就是被代理对象
- Proxy:代理类。RealSubject的代理
- 根据代理的创建时期,代理模式可以分为静态代理和动态代理
静态代理
静态代理需要我们手动为每个被代理的类创建一个代理类,代理类和被代理类实现相同的接口。
- 优点:简单明了,容易理解。
- 缺点:非常不灵活。如果接口增加一个方法,被代理类和代理类都需要修改。并且,每个业务类都需要一个对应的代理类,会导致类的数量急剧膨胀。
让我们用一个发短信的例子来说明:
// 1. 业务接口
interface SmsService {void send(String message);
}// 2. 业务实现类(被代理对象)
class SmsServiceImpl implements SmsService {@Overridepublic void send(String message) {System.out.println("发送短信: " + message);}
}// 3. 静态代理类
class SmsServiceStaticProxy implements SmsService {private final SmsService target;public SmsServiceStaticProxy(SmsService target) {this.target = target;}@Overridepublic void send(String message) {System.out.println("[静态代理] 发送短信前,进行日志记录...");// 调用原始对象的方法target.send(message);System.out.println("[静态代理] 发送短信后,操作完成。");}
}// 使用
public static void main(String[] args) {SmsService smsService = new SmsServiceImpl();SmsServiceStaticProxy proxy = new SmsServiceStaticProxy(smsService);proxy.send("Hello, Static Proxy!");
}
动态代理
由于静态代理的局限性,动态代理应运而生。它不需要我们手动创建代理类,而是在程序运行时动态地生成代理对象。Spring 主要使用两种动态代理技术:
JDK 动态代理
这是 Java 官方提供的代理方式,它要求被代理的类必须实现至少一个接口。它的核心是 java.lang.reflect.Proxy
类和 InvocationHandler
接口。
定义 JDK 动态代理类 (JDKInvocationHandler
):
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;// JDK代理工厂
class JdkProxyFactory {public static Object getProxy(Object target) {return Proxy.newProxyInstance(target.getClass().getClassLoader(), // 目标类的类加载器target.getClass().getInterfaces(), // 目标类实现的接口new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("[JDK动态代理] 方法 " + method.getName() + " 执行前...");Object result = method.invoke(target, args); // 执行目标方法System.out.println("[JDK动态代理] 方法 " + method.getName() + " 执行后...");return result;}});}
}// 使用
public static void main(String[] args) {SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());smsService.send("Hello, JDK Proxy!");
}
代码简单讲解:
InvocationHandler
:
InvocationHandler
接口是 Java 动态代理的关键接口之一,它定义了一个单一方法invoke()
,用于处理被代理对象的方法调用。public interface InvocationHandler {// proxy: 代理对象// method: 代理对象调用的实际方法,即其中需要增强的方法// args: 方法的参数Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
Proxy
:
Proxy
类中使用的最高频率的方法是newProxyInstance()
,这个方法主要用来生成一个代理对象。public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
loader
: 类加载器,用于加载代理对象。interfaces
: 被代理类实现的一组接口(这个参数的定义,也决定了 JDK 动态代理只能代理实现了接口的类)。h
: 实现InvocationHandler
接口的对象。
CGLIB 动态代理
JDK 动态代理有一个最致命的缺陷是其只能代理实现了接口的类。在有些场景下,业务代码是直接实现的类,并没有实现接口。为了解决这个问题,可以使用 CGLIB 动态代理机制来解决。
CGLIB 动态代理的特点:
CGLIB (Code Generation Library) 是一个基于 ASM 的字节码生成库,它允许在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理,很多知名的开源框架都使用了 CGLIB,例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,默认采用 JDK 代理,否则采用 CGLIB 代理。
CGLIB 动态代理实现步骤:
- 定义一个类(被代理类)。
- 自定义
MethodInterceptor
并重写intercept
方法,intercept
用于增强目标方法。 - 通过
Enhancer
类的create()
创建代理类(子类)。 - 因此,它不要求被代理类实现接口,但要求该类不能是
final
的,方法也不能是final
的。
接下来看下实现:
JDK 动态代理不同,CGLIB (Code Generation Library) 实际是属于一个开源项目,如果需要使用它的话,需要手动添加相关依赖。
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
自定义 MethodInterceptor
(方法拦截器) (CGLIBInterceptor
):
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;// CGLIB代理工厂
class CglibProxyFactory {public static Object getProxy(Object target) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(target.getClass()); // 设置父类(被代理类)enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("[CGLIB动态代理] 方法 " + method.getName() + " 执行前...");Object result = method.invoke(target, args); // 执行目标方法System.out.println("[CGLIB动态代理] 方法 " + method.getName() + " 执行后...");return result;}});return enhancer.create(); // 创建代理对象}
}// 使用(假设 SmsServiceImpl 没有实现接口)
public static void main(String[] args) {SmsServiceImpl smsService = (SmsServiceImpl) CglibProxyFactory.getProxy(new SmsServiceImpl());smsService.send("Hello, CGLIB Proxy!");
}
Spring 如何选择代理方式?
- 如果目标对象实现了接口,Spring AOP 默认会使用 JDK 动态代理。
- 如果目标对象没有实现接口,Spring AOP 会使用 CGLIB 动态代理。
- 在 Spring Boot 2.x 之后,默认的代理方式改为了 CGLIB。你也可以通过配置文件
spring.aop.proxy-target-class=false
来强制使用 JDK 动态代理(前提是目标类实现了接口)。
代码简单讲解:
MethodInterceptor
:
MethodInterceptor
和 JDK 代理中的InvocationHandler
类似,它只定义了一个方法intercept()
,用于增强目标方法。public interface MethodInterceptor extends Callback {/*** 参数说明:* o: 被代理的对象* method: 目标方法(被拦截的方法,也就是需要增强的方法)* objects: 方法入参* methodProxy: 用于调用原始方法*/Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable; }
Enhancer.create()
:
Enhancer.create()
用来生成一个代理对象。public static Object create(Class type, Callback callback) {// ...省略 }
type
: 被代理类的类型(类或接口)。callback
: 自定义方法拦截器MethodInterceptor
。
AOP 的一个重要“陷阱”:方法内部调用失效
当一个被 AOP 增强的 Bean,其内部的一个方法 methodB()
调用了另一个被增强的方法 methodA()
时,methodA()
的增强(如 @Transactional
或我们自定义的切面)会失效。
@Service
public class OrderService {@Transactional // 我们希望这个方法有事务public void createOrder() {// ... 业务逻辑 ...}public void processOrders() {// ... 其他逻辑 ...// 这种调用方式,createOrder() 的事务会失效!this.createOrder();}
}
原因:AOP 是通过代理对象实现的。外部调用 orderService.processOrders()
时,调用的是代理对象的方法。但是,当 processOrders()
内部执行 this.createOrder()
时,这里的 this
指向的是原始的 OrderService 对象,而不是代理对象。这次调用绕过了代理,直接访问了原始对象的方法,因此所有的切面逻辑都不会被触发。
如何解决?
-
注入自己:将自身的代理对象注入进来,通过代理对象来调用。
@Service public class OrderService {@Autowiredprivate OrderService self; // 注入自身的代理对象@Transactionalpublic void createOrder() { ... }public void processOrders() {// 通过代理对象调用self.createOrder();} }
注意:这可能会导致循环依赖问题,需要 Spring Boot 2.6+ 或额外配置来解决
-
使用
AopContext
:更优雅的方式是使用AopContext
来获取当前的代理对象。@Service public class OrderService {@Transactionalpublic void createOrder() { ... }public void processOrders() {// 获取当前代理对象并调用((OrderService) AopContext.currentProxy()).createOrder();} }
为了使
AopContext.currentProxy()
生效,需要在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)
。
总 结
- AOP是一种思想,是对某一类事情的集中处理。Spring框架实现了AOP,称之为SpringAOP
- Spring AOP常见实现方式有两种:1. 基于注解@Aspect来实现 2. 基于自定义注解来实现,还有一些更原始的方式,比如基于代理,基于xml配置的方式,但目标比较少见
- Spring AOP 是基于动态代理实现的,有两种方式:1. 基本JDK动态代理实现 2. 基于CGLIB动态代理实现。运行时使用哪种方式与项目配置和代理的对象有关。