跳到主要内容Spring AOP 核心概念、注解与底层原理实战 | 极客日志Javajava
Spring AOP 核心概念、注解与底层原理实战
Spring AOP 通过面向切面编程将日志、事务等横切关注点从业务逻辑中分离。文章对比了硬编码与 AOP 实现的差异,详细解析了切入点、通知、切面等核心术语。涵盖 @Aspect、@Pointcut、@Around 等关键注解用法,以及 @annotation 和 @Order 的高级配置。深入探讨了代理模式,包括 JDK 动态代理与 CGLIB 的实现机制及区别,帮助开发者理解 AOP 底层原理并应用于实际项目。
未来可期1 浏览 概述
面向切面编程 (AOP):是一种编程范式,旨在将横切关注点(如日志、事务、安全等)从业务逻辑中分离出来,通过模块化的方式增强代码的可维护性和复用性。核心思想是通过'切面'定义通用功能,并在运行时动态织入到目标代码中。
横切关注点:指的是在系统中'横向'跨越多个模块、多个层次的功能需求,它们无法很好地被封装在单个类或模块中。
场景举例:监控业务性能
硬编码实现
@Slf4j
public class HardCoding {
public void demo() {
long startTime = System.currentTimeMillis();
log.info("消耗时间:{}", System.currentTimeMillis() - startTime);
}
public static void main(String[] args) {
new HardCoding().demo();
}
}
使用这种硬编码方式监控业务性能主要有以下缺点:
- 代码侵入性强:业务代码与监控代码耦合,修改监控代码会影响业务代码
- 重复代码多:每个方法都要重复编写监控代码,维护困难
- 不利于管理:监控逻辑分散,难以统一管理
- 容易遗漏:开发人员可能忘记添加监控代码
AOP 实现
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>3.4.8</version>
</dependency>
@Slf4j
@RestController
@RequestMapping("/demo")
public class Controller {
@RequestMapping("/a")
public void methodA() {
log.info("执行 methodA");
}
@RequestMapping("/b")
public void methodB() {
log.info("执行 methodB");
}
@RequestMapping("/c")
public void methodC() {
log.info("执行 methodC");
}
}
@Component
@Slf4j
@Aspect
public class AOP {
@Around("execution(* org.example.springaop.blog_demo.Controller.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
log.info("消耗时间:{}", System.currentTimeMillis() - startTime);
return result;
}
}
相较于硬编码的方式,使用 Spring AOP 监控业务性能有很多显著好处:
- 代码解耦:业务代码与监控代码完全分离,易于维护
- 统一管理:所有监控逻辑集中处理,保持一致性和规范性
- 无侵入性:对现有代码零侵入,新增监控不影响业务逻辑
- 复用性强:一套监控方案可以应用到整个项目中的所有方法
核心术语
- 切入点 (Pointcut):提供一个表达式,用于指定对哪些方法进行增强。例如,上图箭头切到的
ABC 业务的集合 为一个切入点,具体能切到哪些方法与表达式有关。
- 连接点 (Join Point):满足切点表达式的方法。上图中处理业务 A/B/C 的执行前后均为连接点 (切入点包含连接点)。
- 通知 (Advice):连接点的共性功能 (重复的逻辑)。如上图箭头执行的逻辑。
- 切面 (Aspect):定义了在何处 (切入点) 和何时 (通知) 执行额外逻辑,即
切入点 + 通知。
Spring AOP 核心注解
@Aspect
作用:用于标识一个类为切面 (Aspect)。切面包含切入点 (Pointcut) 和通知 (Advice),用于模块化横切关注点 (如日志、事务、权限等)。
切入点
execution:是 Spring AOP 中定义切点的一种表达式,用于指定在哪些方法或类上应用通知 (Advice)。它将横切关注点 (如日志、事务) 与业务逻辑分离,通过表达式匹配目标方法或类。
// 语法结构
execution(<访问限定修饰符><返回类型><包名。类名。方法 (方法参数)><异常>)
*:匹配任意字符 (除包分隔符外)
..:匹配任意子包或多级目录;匹配任意数量参数
@Pointcut:是 Spring AOP 中的一个注解,用于定义一个可重用的切点表达式。
通知类型
- ① Around 注解:最强大的通知类型,可以在目标方法执行前后完全控制其行为。它需要接收一个 ProceedingJoinPoint 类型参数,通过调用 proceedingJoinPoint.proceed() 来执行目标方法。Around 通知可以修改返回值、处理异常或完全阻止目标方法执行。
- ② Before 注解:在目标方法执行前触发,无法阻止方法执行 (除非抛出异常)。
- ③ After 注解:在目标方法完成后执行,无论方法是正常返回还是抛出异常。
- ④ AfterReturning 注解:在目标方法正常返回后执行,可以通过访问 (不能修改) 返回值 returning 属性:指定接收返回值的参数名,参数类型必须与目标方法返回类型兼容。
- ⑤ AfterThrowing 注解:只在目标方法抛出异常时执行 throwing 属性:用于绑定目标方法抛出的异常对象。
@Slf4j
@RestController
@RequestMapping("/demo")
public class Controller {
@RequestMapping("/a")
public Object methodA(Integer id) {
log.info("执行 methodA");
return id;
}
@RequestMapping("/b")
public void methodB() {
log.info("执行 methodB");
throw new RuntimeException("发生异常");
}
}
@Component
@Slf4j
@Aspect
public class AOP {
@Pointcut("execution(* org.example.springaop.blog_demo.Controller.*(..))")
public void pointcut() {}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("doAround: 业务执行前");
Object result = joinPoint.proceed();
log.info("doAround: 业务执行后,result:{}", result);
return "success";
}
@Before("pointcut()")
public void doBefore() {
log.info("doBefore");
}
@After("pointcut()")
public void doAfter() {
log.info("doAfter");
}
@AfterReturning(value = "pointcut()", returning = "id")
public void doAfterReturning(Integer id) {
log.info("doAfterReturning,id:{}", id);
}
@AfterThrowing(value = "pointcut()", throwing = "throwable")
public void doAfterThrowing(Throwable throwable) {
log.info("doAfterThrowing,throwable:{}", throwable.getMessage());
}
}
当有异常抛出时,URL:127.0.0.1:8080/demo/b,控制台会捕获异常并执行 AfterThrowing 逻辑。
当无异常抛出时,URL:127.0.0.1:8080/demo/a?id=1,控制台会正常返回结果并执行 AfterReturning 逻辑。
@annotation
作用:@annotation 表达式用于匹配带有指定注解的方法,是 AOP 中实现精准切入的关键方式之一。通过该表达式,可以拦截被特定注解标记的方法,实现逻辑增强。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomizeAspect {}
@Around("@annotation(org.example.springaop.config.CustomizeAspect)")
public Object customize(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("customize before");
Object result = joinPoint.proceed();
log.info("customize after,result:{}", result);
return "success";
}
@RequestMapping("/c")
@CustomizeAspect
public Object methodC(Integer id) {
log.info("执行 methodC");
return id;
}
@Order
作用:用于指定 Bean 的加载顺序,其核心作用是通过数值定义优先级,数值越小优先级越高。
@Component
@Slf4j
@Aspect
@Order(1)
public class AOP1 {
}
@Component
@Slf4j
@Aspect
@Order(2)
public class AOP2 {
}
执行时会先执行 Order(1) 的逻辑,再执行 Order(2) 的逻辑。
AOP 底层原理
代理模式
代理模式 (Proxy Pattern):通过创建一个代理对象来控制对目标对象的访问。代理对象作为目标对象的替代品,可以在访问前后添加额外逻辑 (如权限控制、性能监控等)。
静态代理
代理类在编译期确定,需要手动为每个目标类编写代理类。以租房为例:租客 (调用方)、房东 (目标对象)、中介 (代理对象)。
public interface IHouse {
void rent();
void sell();
}
public class RealHouse implements IHouse {
@Override
public void rent() {
System.out.println("房东出租房子");
}
@Override
public void sell() {
System.out.println("房东售卖房子");
}
}
public class HouseProxy implements IHouse {
private final IHouse realHouse;
public HouseProxy(IHouse realHouse) {
this.realHouse = realHouse;
}
@Override
public void rent() {
System.out.println("开始代理");
realHouse.rent();
System.out.println("结束代理");
}
@Override
public void sell() {
System.out.println("开始代理");
realHouse.sell();
System.out.println("结束代理");
}
public static void main(String[] args) {
IHouse iHouse = new HouseProxy(new RealHouse());
iHouse.rent();
iHouse.sell();
}
}
执行结果:
开始代理
房东出租房子
结束代理
开始代理
房东售卖房子
结束代理
动态代理
AOP 的底层原理依赖于动态代理:不需要针对每一个目标对象创建一个代理对象,而是将代理对象的创建时机推迟到程序运行时交由 JVM 完成。
JDK 动态代理
JDK 动态代理:是 Java 标准库提供的方式,要求目标对象必须实现接口。通过java.lang.reflect.Proxy 类和java.lang.reflect.InvocationHandler 接口动态生成代理类,生成的代理对象和目标对象实现自同一接口 (而非与目标类本身有直接继承关系),与上述静态代理类似,但把代理对象的生成时机推迟到程序运行时。
Proxy.newProxyInstance() 是 Java 动态代理的核心方法,用于在运行时创建代理对象。该方法接收三个参数:
- ClassLoader loader:用于加载代理类的类加载器。通常传入目标类的类加载器,确保代理类与目标类在同一个类加载器环境中。代理对象需要实现目标接口,其字节码由 Proxy 工具类动态生成。通过目标类加载器创建代理类,可保证类型系统的一致性。
- Class<?>[] interfaces:目标对象实现的接口数组。代理类会实现这些接口,并将方法调用转发到 InvocationHandler。
- InvocationHandler h:负责处理代理对象的方法调用。
@Slf4j
public class JDKInvocationHandler implements InvocationHandler {
private final Object realHouse;
public JDKInvocationHandler(Object realHouse) {
this.realHouse = realHouse;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("JDK 开始代理");
Object invoke = method.invoke(realHouse, args);
log.info("JDK 结束代理,invoke:{}", invoke);
return invoke;
}
}
public class Main {
public static void main(String[] args) {
IHouse target = new RealHouse();
IHouse iHouse = (IHouse) Proxy.newProxyInstance(RealHouse.class.getClassLoader(), new Class[]{IHouse.class}, new JDKInvocationHandler(target));
iHouse.rent();
iHouse.sell();
}
}
CGLIB 动态代理
CGLIB (Code Generation Library):是第三方库,基于字节码增强 (动态生成目标类的子类字节码,重写方法逻辑),通过继承方式实现代理,不要求目标对象实现接口。通过org.springframework.cglib.proxy.Enhancer 类动态生成目标类的子类作为代理。
@Slf4j
public class CGLibMethodInterceptor implements MethodInterceptor {
private final Object realHouse;
public CGLibMethodInterceptor(Object realHouse) {
this.realHouse = realHouse;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
log.info("开始代理");
IHouse result = (IHouse) method.invoke(realHouse, args);
log.info("结束代理");
return result;
}
}
public class Main {
public static void main(String[] args) {
RealHouse target = new RealHouse();
RealHouse realHouse = (RealHouse) Enhancer.create(RealHouse.class, new CGLibMethodInterceptor(target));
realHouse.rent();
realHouse.sell();
}
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online