Java案例实战:如何实现AOP切面编程?——从原理到项目落地
目录导读
- AOP核心概念与适用场景
- 什么是切面、连接点、通知、切入点
- 哪些业务场景需要AOP(日志、事务、权限)
- 主流实现方案对比
- Spring AOP vs AspectJ
- 代理模式选择:JDK动态代理 vs CGLIB
- 手写第一个AOP例子:日志切面(Spring Boot)
- 依赖配置与注解启用
- 定义切面类并声明通知(@Before/@After/@Around)
- 进阶实战:自定义注解驱动权限校验
- 创建@PermissionCheck注解
- 在切面中解析注解并实现逻辑
- 常见问题与性能优化
- 同一类内部调用失效怎么办?
- 如何避免过多切面导致性能下降?
- 深层问答环节
- Q1:JDK代理和CGLIB何时选用?
- Q2:如果多个切面同时作用于同一方法,执行顺序如何控制?
AOP核心概念与适用场景
AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式,它允许开发者将横切关注点(如日志记录、事务管理、安全校验)从业务逻辑中剥离出来,独立成“切面模块”,从而避免代码冗余和耦合。

关键术语需要先透彻理解:
- 切面(Aspect):横切关注点的模块化,例如
LoggingAspect。 - 连接点(Join Point):程序执行过程中的特定位置,如方法调用、异常抛出。
- 通知(Advice):切面在特定连接点执行的动作,有
@Before、@After、@Around、@AfterReturning、@AfterThrowing五种类型。 - 切入点(Pointcut):通过表达式定义“哪些连接点需要执行通知”。
- 引入(Introduction):为对象添加新方法或属性。
- 目标对象(Target):被通知的对象。
- 织入(Weaving):将切面应用到目标对象的过程(编译期、加载期或运行期)。
经典适用场景:
| 场景 | 原因 |
| --- | --- |
| 日志记录 | 避免每个方法手动插入日志代码 |
| 事务管理 | 数据层操作统一加事务控制 |
| 权限校验 | 全局拦截未授权调用 |
| 性能监控 | 统计方法执行时间 |
| 缓存管理 | 针对查询方法自动管理缓存 |
主流实现方案对比:Spring AOP vs AspectJ
| 特性 | Spring AOP | AspectJ |
|---|---|---|
| 实现基础 | 基于动态代理(JDK或CGLIB) | 基于字节码修改(编译期/加载期织入) |
| 性能 | 运行时生成代理,略低 | 编译期增强,性能更好 |
| 支持粒度 | 仅方法级别连接点 | 支持字段、构造器等更多连接点 |
| 复杂度 | 低,与Spring深度集成 | 较高,需单独依赖或配置 |
企业级开发中,Spring AOP已覆盖95%需求,若需拦截私有方法或属性访问,才考虑引入AspectJ,本文以Spring AOP + 注解驱动为例。
代理模式选择规则:
- 目标类实现了接口 → Spring优先使用JDK动态代理。
- 目标类没有实现接口 → 使用CGLIB生成子类代理。
- 可在
@EnableAspectJAutoProxy(proxyTargetClass = true)中强制CGLIB。
手写第一个AOP例子:日志切面
环境与依赖
确保pom.xml包含:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
启动类增加@EnableAspectJAutoProxy注解。
定义切面类
@Aspect
@Component
public class LogAspect {
// 使用@Pointcut定义切入点:匹配所有service包下的任意方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// @Before:方法执行前打印入参
@Before("servicePointcut()")
public void logBefore(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
System.out.println("[AOP] 调用方法:" + joinPoint.getSignature().getName()
+ ",参数:" + Arrays.toString(args));
}
// @Around:封装方法执行,可控制返回值
@Around("servicePointcut()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行原方法
long duration = System.currentTimeMillis() - start;
System.out.println("[AOP] 方法" + pjp.getSignature().getName()
+ "执行耗时:" + duration + "ms");
return result;
}
// @AfterReturning:方法正常返回后记录结果
@AfterReturning(pointcut = "servicePointcut()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("[AOP] 返回结果:" + result);
}
}
测试业务类
@Service
public class OrderService {
public String createOrder(String userId, double amount) {
return "订单创建成功:" + userId + " 金额:" + amount;
}
}
输出示例:
[AOP] 调用方法:createOrder,参数:[user01, 99.8]
[AOP] 方法createOrder执行耗时:2ms
[AOP] 返回结果:订单创建成功:user01 金额:99.8
进阶实战:自定义注解驱动权限校验
创建自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionCheck {
String value() default ""; // 权限标识
}
切面实现校验逻辑
@Aspect
@Component
public class PermissionAspect {
// 切入点匹配所有含有@PermissionCheck的方法
@Pointcut("@annotation(com.example.annotation.PermissionCheck)")
public void permissionPointcut() {}
@Around("permissionPointcut()")
public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
PermissionCheck permissionCheck = signature.getMethod()
.getAnnotation(PermissionCheck.class);
String requiredPermission = permissionCheck.value();
// 模拟从当前上下文获取用户权限列表
List<String> userPermissions = getCurrentUserPermissions();
if (!userPermissions.contains(requiredPermission)) {
throw new SecurityException("无权限执行:" + requiredPermission);
}
return pjp.proceed(); // 权限校验通过
}
private List<String> getCurrentUserPermissions() {
return Arrays.asList("read", "write");
}
}
业务方法使用注解
@RestController
public class AdminController {
@GetMapping("/deleteUser")
@PermissionCheck("admin:delete")
public String deleteUser() {
return "用户已删除";
}
}
优势:通过注解即可声明式控制权限,业务代码零侵入。
常见问题与性能优化
问题1:同一类内部调用失效
@Transactional
public void outerMethod() {
this.innerMethod(); // 此时AOP不会生效!因为this是原始对象而非代理
}
解决:
- 注入自身代理:
@Autowired ApplicationContextcontext.getBean(Xxx.class)。 - 使用
AopContext.currentProxy()(需要暴露代理:@EnableAspectJAutoProxy(exposeProxy = true))。
问题2:多切面执行顺序控制
使用@Order注解指定顺序,数值越小越先执行@Before:
@Aspect
@Order(1)
public class LogAspect {...}
@Aspect
@Order(2)
public class PermissionAspect {...}
性能优化建议
- 避免在
@Before和@After中执行耗时操作(如数据库查询)。 - 使用
cglib-proxy时,目标类不能是final。 - 切面数量尽量控制在5个以内,过多会拖慢每次方法调用。
- 利用编译期织入(AspectJ)可以消除运行时代理开销。
深层问答环节
Q1:JDK代理和CGLIB动态代理何时选用?性能差距大吗?
答:
- JDK代理:要求目标类实现接口,通过
InvocationHandler拦截方法,运行效率较高,因为生成的代理类只包含接口中的方法。 - CGLIB代理:通过继承目标类生成子类,不能代理
final类和final方法,启动时需生成字节码,首次创建较慢,但运行期性能接近JDK代理。
选型建议:若项目已全部面向接口编程,优先使用JDK代理;若存在无接口的第三方类,强制指定CGLIB,在Spring Boot 2.x+中默认已优化:当目标类实现接口时用JDK,否则用CGLIB。
Q2:如果多个切面同时作用于同一方法,执行顺序由什么决定?
答:
- Spring AOP:按切面类上的
@Order注解(或实现Ordered接口)的数值升序执行。@Before:数值越小越先执行。@After、@AfterReturning、@AfterThrowing:数值越大越先执行(实际上往往以逆向顺序执行)。
- AspectJ:按织入时声明的顺序或依赖
declare precedence语句控制。
最佳实践:如果不确定顺序,让日志切面排最外层(@Order(1)),事务切面排内层(@Order(100)),这样即使权限校验失败,日志依然能记录拒绝原因。
AOP切面是Java企业级开发中解耦横切逻辑的利器,从最基础的日志切面到自定义注解驱动的权限校验,核心在于理解代理机制和切入点表达式,实际项目中,合理利用@Order和@Pointcut复用,并注意内部调用失效问题,即可实现高内聚低耦合的优雅代码结构。