AOP技术详解
思想概念
概念
-
横切关注点
- 横切关注点通常包括日志记录、事务管理、安全性、性能监控等,它们跨越多个类和模块,并且与业务逻辑代码正交
-
切面
- AOP中,这些横切关注点被封装在“切面”(Aspect)中
- 切面定义了何时(when)、何地(where)以及如何(how)应用这些横切逻辑。切面可以看作是一个包含通知(Advice)和切点(Pointcut)的模块
-
通知
- 通知定义了横切逻辑的具体实现
-
切点
- 切点则定义了通知应该应用到哪些连接点(Join Point)上
-
连接点
- 连接点是程序执行过程中的一个特定点,如方法的调用或异常的处理
简介
- 面向切面编程,是对面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
核心思想
- 将横切关注点与业务逻辑代码分离,以提高代码的可维护性、可重用性和模块化。通过将横切关注点封装在切面中,可以避免在多个类中重复编写相同的代码,并且可以在不修改现有代码的情况下添加新的横切逻辑
SpringAOP
Spring AOP则是Spring框架的一部分,它提供了基于代理的AOP实现方式,并支持AspectJ的注解
SpringAOP是Spring框架的一部分,与Spring的其他功能(如事务管理、安全等)紧密集成,提供了方便的扩展和定制能力
Spring AOP使用纯Java实现,通过运行时动态代理的方式(基于JDK动态代理或CGLIB)向目标类织入增强代码
-
JDK动态代理
- Spring AOP默认使用JDK Dynamic Proxy来生成代理对象
- JDK动态代理是基于接口的代理,它要求目标类必须实现一个或多个接口
- 如果目标类实现了接口,Spring会默认使用JDK动态代理
- JDK动态代理通过实现InvocationHandler接口来调用原对象的实现,并且所有要被代理的方法都必须是接口中声明的方法
-
CGLIB动态代理
- 如果目标对象没有实现接口,Spring AOP则会使用CGLIB(Code Generation Library)来生成代理对象
- CGLIB动态代理是一种基于继承的代理,它不要求目标类实现任何接口
- 它通过生成目标类的子类来实现代理,因此即使目标类是一个普通的类,也能够进行代理
- CGLib动态代理的性能通常比JDK动态代理要好,但可能会因为继承的问题导致某些问题,比如无法代理final方法
-
总结
- Spring AOP在创建代理对象时,会首先检查目标类是否实现了接口
- 如果目标类实现了接口,Spring AOP会默认使用JDK动态代理
- 如果目标类没有实现接口,Spring AOP则会使用CGLIB动态代理
常用的Spring AOP注解及其用法
-
@Aspect
-
用于声明一个类为切面类
-
使用方式:在类定义上添加此注解
- @Aspect
@Component
public class LoggingAspect {
// …
}
- @Aspect
-
-
@Pointcut
-
用于定义一个切点(pointcut),即指定哪些方法的执行需要被拦截
-
使用方式:在方法上添加此注解,并定义切点表达式
- @Pointcut(“execution(* com.example.myapp.service..(…))”)
public void serviceMethods() {}
- @Pointcut(“execution(* com.example.myapp.service..(…))”)
-
-
@Before前置通知
-
在目标方法执行之前执行切面的通知(advice)
-
使用方式:在切面类的方法上添加此注解,并指定一个切点表达式或引用一个已定义的切点
- @Before(“serviceMethods()”)
public void beforeAdvice(JoinPoint joinPoint) {
// …
}
- @Before(“serviceMethods()”)
-
-
@After后置通知
-
在目标方法执行之后执行切面的通知(无论方法执行是否成功)
-
使用方式:与@Before类似
- @After(“serviceMethods()”)
public void afterAdvice(JoinPoint joinPoint) {
// …
}
- @After(“serviceMethods()”)
-
-
@AfterReturning 后置返回通知
-
在目标方法正常执行完毕后执行切面的通知(即方法没有抛出异常)
-
可以使用returning属性指定一个参数名来接收目标方法的返回值
- @AfterReturning(pointcut = “serviceMethods()”, returning = “retVal”)
public void afterReturningAdvice(JoinPoint joinPoint, Object retVal) {
// …
}
- @AfterReturning(pointcut = “serviceMethods()”, returning = “retVal”)
-
-
@AfterThrowing后置异常通知
-
在目标方法抛出异常后执行切面的通知
-
可以使用throwing属性指定一个参数名来接收抛出的异常
- @AfterThrowing(pointcut = “serviceMethods()”, throwing = “ex”)
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
// …
}
- @AfterThrowing(pointcut = “serviceMethods()”, throwing = “ex”)
-
-
@Around环绕通知
-
在目标方法执行前后执行切面的通知,并可以控制目标方法的执行
-
通知方法需要返回一个Object类型的值,代表目标方法的返回值
- @Around(“serviceMethods()”)
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// …
Object result = joinPoint.proceed(); // 执行目标方法
// …
return result;
}
- @Around(“serviceMethods()”)
-
-
@EnableAspectJAutoProxy
-
用于开启对AspectJ自动代理的支持
-
通常将此注解添加在配置类上,以启用AOP功能
- @Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// …
}
- @Configuration
-
-
注意事项
- 在使用Spring AOP时,还需要确保你的Spring配置启用了AOP支持(例如,通过@EnableAspectJAutoProxy),并且你的切面类是一个Spring组件(例如,通过@Component)。同时,如果你的切面使用了AspectJ的切点表达式,你可能需要在你的项目中包含AspectJ的依赖
AspectJ
一个完整的AOP框架,它提供了丰富的AOP实现方式和语法
AspectJ可以作为独立的框架使用,不依赖spring环境
AspectJ本身不直接使用动态代理技术,而是通过编译时或类加载时织入的方式来实现AOP
- 这通过AspectJ的编译器和类加载器来完成,而不是直接依赖动态代理技术
AspectJ的AOP实现包括编译时织入(Compile-time weaving)和加载时织入(Load-time weaving,简称LTW)。编译时织入是在编译目标类时将切面逻辑添加到目标类的字节码中;而加载时织入则是在类加载到JVM之前,通过自定义的类加载器或类文件转换器将切面逻辑添加到目标类的字节码中
AspectJ的切点表达式
-
用于定义哪些方法调用应该被拦截或通知的表达式。这些表达式基于方法签名和运行时信息来定义切点,允许你精确地指定哪些方法或方法的组合应该触发通知(advice)
-
常见的切点表达式元素和用法
-
方法签名匹配
- execution(* com.example.myapp.MyClass.myMethod(…)):匹配MyClass类中名为myMethod的所有方法调用,不论参数如何
- execution(public * *(…)):匹配所有公共方法的调用
- execution(* *(String, …)):匹配所有以String类型作为第一个参数的方法调用
-
访问修饰符
- public execution(* *(…)):只匹配公共方法的调用
- protected execution(* *(…)):只匹配受保护方法的调用
-
参数类型
- execution(* com.example.myapp.MyClass.myMethod(String)):匹配MyClass类中名为myMethod且只有一个String类型参数的方法调用
- execution(* com.example.myapp.MyClass.myMethod(String, …)):匹配MyClass类中名为myMethod且第一个参数为String的方法调用,无论是否有其他参数
-
通配符
- *:代表任意类型或任意方法名
- …:代表任意数量的参数,包括零个
-
逻辑运算符
- &&:逻辑与
- ||:逻辑或
- !:逻辑非
-
复合切点
可以通过命名切点(named pointcut)来定义可重用的切点表达式,并在其他地方引用它
@Pointcut("execution(* com.example.myapp.*.*(..))") public void anyMethodInMyApp() {} @Before("anyMethodInMyApp()") public void beforeAdvice(JoinPoint joinPoint) { }
-
- 运行时测试AspectJ还支持基于运行时信息的切点表达式,如this(), target(), args(), @annotation(), @within(), @target(), @args()等- 示例- @annotation(com.example.MyAnnotation):匹配所有被MyAnnotation注解的方法调用- target(com.example.MyClass):匹配目标对象是MyClass实例的所有方法调用- args(String, ..):匹配所有以String类型作为第一个参数的方法调用- 切点组合
可以组合多个切点表达式来创建一个更复杂的切点,例如execution(* *..*(..)) && !@annotation(org.springframework.transaction.annotation.Transactional),这会匹配所有没有@Transactional注解的方法调用
## 性能比较### AspectJ:提供了完整的AOP实现,包括编译时和加载时的织入,因此功能强大且性能较好### Spring AOP:主要解决企业业务开发中的常见问题,提供了基于代理的AOP实现,但在某些复杂场景下可能不如AspectJ灵活## 使用场景比较### 当需要处理跨多个类和模块的横切关注点(如日志、事务、安全性等)时,AspectJ和Spring AOP都是很好的选择。但在某些需要更精细控制或更复杂逻辑的场景下,AspectJ可能更具优势### AspectJ可以作为独立的框架使用,在非spring环境下可以使用AspectJ实现AOP功能## aop失效场景### final方法调用:由于AOP是通过创建代理对象来实现的,而无法对final方法进行子类化和覆盖,所以无法拦截这些方法。当调用final修饰的方法时,AOP代理会失效### 静态方法调用:静态方法属于类本身,不属于类的实例,因此AOP代理无法对其进行拦截。调用静态方法时,AOP代理会失效### 私有方法调用:私有方法只能在类的内部被调用,并且不能被继承或重写。因此,AOP代理无法对私有方法进行拦截。当在类的内部调用私有方法时,AOP代理会失效### 类内部自调用:如果一个类中的方法A调用了同类的另一个方法B(即方法A内部直接调用方法B),那么由于方法A和方法B属于同一个类的实例,AOP代理无法对方法B进行拦截。这种情况下,AOP代理会失效### 内部类方法调用:如果一个类中的方法调用了其内部类的方法,由于内部类方法的调用是在类的内部进行的,AOP代理无法对内部类方法进行拦截。这种情况下,AOP代理也会失效### 通过this调用本类方法:在Java中,使用this关键字可以引用当前类的实例。如果通过this关键字调用本类的方法,那么AOP代理无法对该方法进行拦截,从而导致AOP失效### AfterReturning注解,方法未正常结束:在使用AspectJ等AOP框架时,如果使用@AfterReturning注解来处理方法的返回值,但当方法因为抛出异常而未能正常结束时,AOP代理可能无法正确执行@AfterReturning注解指定的操作。这种情况下,虽然AOP本身没有失效,但相关的通知(Advice)可能无法按照预期执行## 避免AOP失效的措施### 尽量避免在类的内部直接调用其他方法,而是将方法调用放在类的外部或通过接口进行调用### 对于需要被AOP拦截的方法,不要使用final、static或private修饰符进行修饰### 对于内部类方法的调用,可以通过将内部类的方法暴露为公共接口的方法来进行调用,从而确保AOP代理能够对其进行拦截### 在使用@AfterReturning等注解时,确保方法能够正常结束并返回结果,避免因为异常而导致通知无法执行