深入掌握Spring AOP:从原理到实战的完整指南
一、AOP核心概念与实战引入
1.1 传统开发中的痛点
以用户登录场景为例,假设我们有一个基础登录功能:
public class LoginService {
public void login(String username, String password) {
// 验证用户名密码
System.out.println("核心登录逻辑执行");
}
}
现在需要新增权限校验功能,传统方案有两种:
- 修改源代码:侵入性强,违反开闭原则
- 纵向继承扩展:产生冗余代码,维护困难
1.2 AOP的破局之道
// 目标类
public class LoginService {
public void login(String username) {
System.out.println(username + "登录成功!");
}
}
// 切面类
@Aspect
@Component
public class AuthAspect {
@Before("execution(* com.example.service.LoginService.login(..))")
public void checkPermission() {
System.out.println("[权限校验] 正在验证用户权限...");
}
}
运行效果:
[权限校验] 正在验证用户权限...
admin登录成功!
二、AOP底层原理深度剖析
1. 代理模式双雄
代理类型 | 实现方式 | 适用场景 |
---|---|---|
JDK动态代理 | 基于接口实现 | 目标类有接口时优先使用 |
CGLIB动态代理 | 生成子类继承目标类 | 无接口或需要高性能场景 |
2. 动态代理实现原理
// JDK动态代理示例
public class JdkProxy implements InvocationHandler {
private Object target;
public Object bind(Object target) {
this.target = target;
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强");
Object result = method.invoke(target, args);
System.out.println("后置增强");
return result;
}
}
三、XML配置方式全解
1.AOP相关的术语
Joinpoint(连接点) 类里面有哪些方法可以增强这些方法称为连接点
Pointcut(切入点) -- 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
Advice(通知/增强)-- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
Aspect(切面)-- 是 切入点+通知 的结合,以后咱们自己来编写和配置的
2. 核心配置模板
<!-- 配置切面 -->
<aop:config>
<aop:aspect ref="logAspect">
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.service.*.*(..))"/>
<aop:before method="logStart" pointcut-ref="servicePointcut"/>
<aop:after-returning method="logEnd" pointcut-ref="servicePointcut"/>
<aop:after-throwing method="logException" pointcut-ref="servicePointcut"/>
</aop:aspect>
</aop:config>
3. 切入点表达式速查表
表达式模式 | 说明 |
---|---|
execution(* *Service.*(..)) | 拦截所有Service类的任意方法 |
execution(* save*(..)) | 拦截所有以save开头的方法 |
within(com.example.dao.*) | 拦截指定包下的所有类方法 |
切入点表达式的格式如下:
execution([修饰符] [返回值类型] [类全路径] [方法名 ( [参数] )])
修饰符可以省略不写,不是必须要出现的。
返回值类型是不能省略不写的,根据你的方法来编写返回值,可以使用 * 代替。
包名,类名,方法名,参数的规则如下:
例如:com.qcby.demo3.BookDaoImpl.save()
首先包名,类名,方法名是不能省略不写的,但是可以使用 * 代替
中间的包名可以使用 * 号代替
类名也可以使用 * 号代替,也有类似的写法:*DaoImpl
方法也可以使用 * 号代替
参数如果是一个参数可以使用 * 号代替,如果想代表任意参数使用 ..
比较通用的表达式:execution(* com.qcby.*.ServiceImpl.save(..))
举例2:com.qcby.demo3.BookDaoImpl当中所有的方法进行增强
execution(* com.qcby.*.ServiceImpl.*(..))
举例3:com.qcby.demo3包当中所有的方法进行增强
execution(* com.qcby.*.*.*(..))
四、注解方式高效开发
1. 全注解配置模板
@Configuration
@EnableAspectJAutoProxy // 开启AOP自动代理
@ComponentScan("com.example")
public class AppConfig {}
@Aspect
@Component
public class LogAspect {
@Around("@annotation(com.example.anno.OperateLog)")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
long time = System.currentTimeMillis() - start;
System.out.println("方法执行耗时:" + time + "ms");
return result;
}
}
2. 通知类型对比表
注解 | 执行时机 | 典型应用场景 |
---|---|---|
@Before | 方法执行前 | 参数校验、权限控制 |
@After | 方法执行后(始终执行) | 资源清理 |
@AfterReturning | 方法正常返回后 | 结果日志记录 |
@AfterThrowing | 方法抛出异常时 | 异常监控报警 |
@Around | 方法执行前后 | 性能监控、事务管 |
@Component
@Aspect //生成代理对象
public class UserProxy {
//增强/通知 ---》前置通知
@Before(value = "execution(* com.*.User.add(..))")
public void before(){
System.out.println("before.............");
}
// 环绕通知
@Around(value = "execution(* com.*.User.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("before.............");
// 执行被增强的方法
proceedingJoinPoint.proceed();
System.out.println("after.............");
}
// 最终通知
@After(value = "execution(* com.*.User.add(..))")
public void after() {
System.out.println("after.............");
}
//异常通知
@AfterThrowing(value = "execution(* com.*.User.add(..))")
public void afterThrowing() {
System.out.println("afterThrowing.............");
}
//后置通知
@AfterReturning(value = "execution(* com.*.User.add(..))")
public void afterReturning() {
System.out.println("afterReturning.............");
}
}
测试类:
@Test
public void aopTest1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
user.add();
}
五、企业级最佳实践
1. 切面优先级控制
@Aspect
@Order(1) // 数字越小优先级越高
public class SecurityAspect {
// 安全检查切面
}
@Aspect
@Order(2)
public class LogAspect {
// 日志记录切面
}
2. 自定义注解实现精准切入
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {}
@Aspect
@Component
public class AuditAspect {
@AfterReturning("@annotation(AuditLog)")
public void auditLog(JoinPoint jp) {
// 审计日志记录逻辑
}
}
六、性能优化与避坑指南
1. AOP性能影响三要素
-
代理类型选择:优先JDK动态代理,必要时使用CGLIB
-
切入点粒度:避免过于宽泛的表达式
-
通知复杂度:减少耗时操作在切面中的使用
2. 常见问题解决方案
问题1:循环依赖导致代理失效
✅ 解决方案:使用Setter注入代替构造器注入
问题2:内部方法调用切面失效
✅ 解决方案:通过AopContext获取代理对象
((Service)AopContext.currentProxy()).internalMethod();