一、Spring AOP简介
Spring AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架的一个核心特性,它允许开发者将横切关注(如日志、事务管理、安全性等)从业务逻辑中分离出来,从而提高代码的可重用性、可维护性和可扩展性。
二、Spring AOP相关概念
- 切面(Aspect):切面是横切关注点的模块化,它包含了切点和通知。在Spring中,可以通过使用@Aspect注解将一个类定义为切面。
- 切点(Pointcut):切点用于定义通知应该在什么时候触发。它通过匹配连接点来确定通知具体的执行位置。在Spring AOP中,可以使用AspectJ切点表达式(如
execution()
)来匹配单个连接点或多个连接点。 - 通知(Advice):通知是指在切面中定义的具体的横切逻辑,它会在满足特定条件时被执行。根据通知执行的时间点不同,通知又可分为五种类型:
- 前置通知(Before Advice):在目标方法执行之前执行。
- 后置通知(After Returning Advice):在目标方法正常执行后执行。
- 异常通知(After Throwing Advice):在目标方法抛出异常后执行。
- 最终通知(After (Finally) Advice):在目标方法执行完成后执行,无论是否正常结束。
- 环绕通知(Around Advice):包围目标方法的执行,可以在方法执行前后执行自定义逻辑。
- 连接点(Join Point):连接点表示程序执行过程中可能触发通知执行的点,可能是方法的调用或异常的抛出。切点其实就可以看作是具体执行通知的那个连接点。在Spring AOP中,连接点特指方法的执行。
- 目标对象(Target Object):目标对象是被一个或多个通知包围的业务逻辑对象。
- 织入(Weaving):织入是将切面应用到目标对象以创建新的代理对象的过程。Spring AOP支持编译时织入、类加载时织入和运行时织入。
- 代理(Proxy):代理是一个代表另一个目标对象的对象,它实现了目标对象的接口,并在调用目标对象的方法之前或之后插入额外的行为。在Spring AOP中,切面通过创建代理对象来实现。
- 引入(Introduction):引入允许我们向现有类添加新的方法或属性。在Spring AOP中,这是通过声明一个切面并使用@DeclareParents注解来实现的。
三、Spring AOP作用及优点
主要作用:
- 解耦:将业务逻辑与横切逻辑分离,降低模块间的耦合度。
- 增强可维护性:通过集中管理横切关注点,使得业务逻辑更容易理解和维护。
- 提高可重用性:横切逻辑可以作为独立的组件被不同的系统重用。
- 简化开发:减少了重复代码的编写,提高了开发效率。
- 增强功能:可以方便地向系统中添加新的横切功能,如日志、安全性检查等。
优点:
- 模块化:AOP允许开发者将横切关注点模块化,使得代码更加清晰,易于管理。
- 降低复杂性:通过将横切逻辑从业务逻辑中分离出来,降低了代码的复杂性。
- 易于测试:由于横切逻辑与业务逻辑分离,因此更容易对业务逻辑进行单元测试。
- 灵活性:AOP提供了一种灵活的设计机制,可以在不影响原有业务逻辑的前提下,动态地添加或修改功能。
- 可扩展性:通过添加新的切面,可以轻松地扩展系统的功能,而无需修改现有的代码。
四、Spring AOP实现原理
Spring AOP 实现原理基于Java动态代理机制。Spring AOP 提供了两种代理方式:JDK动态代理和CGLIB代理。
JDK动态代理:
- JDK动态代理只能代理实现了接口的类。
- 当创建一个代理对象时,JDK动态代理会在运行时动态地创建一个实现了所需接口的代理类。
- 代理类会覆盖接口中的所有方法,并在方法内部调用目标对象的相应方法,同时可以在调用前后加入额外的逻辑(通知)。
- JDK动态代理是通过反射(Reflection)API来实现的,通过Proxy类和InvocationHandler接口。
- 当客户端调用代理对象的方法时,InvocationHandler的invoke方法会被触发,然后可以执行前置通知(Before advice)、执行目标方法、执行后置通知(After returning advice)等操作。
CGLIB动态代理:
- CGLIB(Code Generation Library)是一个高性能的代码生成库,它可以在运行时扩展Java类和实现Java接口。
- 对于没有实现接口的类,Spring AOP默认使用CGLIB来创建代理对象。相比于JDK动态代理使用更加灵活。
- CGLIB通过继承目标类并重写其非final方法的方式来创建子类代理。
- 代理类中重写的方法会包含调用目标对象方法前后的通知逻辑。
五、Spring AOP 与 AspectJ AOP的区别
Spring AOP和AspectJ AOP都是实现面向切面编程(AOP)的技术,它们在很多方面都相似,但也存在一些关键区别。Spring AOP是一个轻量级的AOP实现,适合大多数Spring应用的基本需求;而AspectJ是一个功能更全面的AOP框架,适用于更复杂和性能敏感的场景。以下是两者的一些不同点:
- 目标:
- Spring AOP主要用于简化企业级应用开发,它侧重于提供一种轻量级的AOP解决方案。
- AspectJ是一个更加强大的AOP框架,提供了全面的AOP功能,它不仅可以用于企业级应用开发,还可以用于各种复杂的AOP场景。
- 实现原理:
- Spring AOP基于代理模式,它使用JDK动态代理和CGLIB库来创建代理对象,只支持方法级别的切面。
- AspectJ是一个独立的AOP框架,它提供了比Spring AOP更丰富的织入时机和方式,包括编译时和类加载时织入,支持字段、方法、构造器等的织入。
- 织入时机:
- Spring AOP支持编译时、类加载时和运行时织入。
- AspectJ支持更多种类的织入时机,包括编译时和加载时织入,并且提供了比Spring AOP更复杂的织入选项。
- 性能:
- 由于Spring AOP是基于代理的,可能会引入额外的性能开销,尤其是当目标对象不适合使用JDK动态代理时(比如接口为空)。
- AspectJ提供了更底层的支持,通常在性能上优于Spring AOP,特别是在复杂的AOP场景下。
- 功能范围:
- Spring AOP被设计为与Spring框架紧密集成,主要用于实现简单的AOP需求,如日志、安全性等。
- AspectJ提供了更全面的AOP功能,包括ITD(Inter-type declarations,跨类型声明),允许开发者在类定义之外添加新的方法、属性等。
- 集成:
- Spring AOP是Spring框架的一部分,与Spring的其他模块(如IoC容器)紧密集成。
- AspectJ虽然可以独立使用,但Spring提供了对AspectJ的支持,允许开发者在Spring应用中使用AspectJ的高级特性。
- 使用场景:
- 对于大多数Spring应用,Spring AOP已经足够满足需求,特别是当与Spring的其他特性(如依赖注入)集成时。
- AspectJ更适合那些需要高级AOP功能或者对性能有严格要求的场景。
六、Spring AOP 切点
Spring AOP使用AspectJ的切点表达式语言,以下是一些常用的切点表达式:
execution(* com.example.service.*.*(..))
:匹配com.example.service包下所有类的所有方法。within(com.example.service.*)
:匹配com.example.service包下所有类。this(com.example.service.MyService)
:匹配实现了MyService接口的所有bean。target(com.example.service.MyService)
:匹配目标对象实现了MyService接口的所有bean。@annotation(org.springframework.transaction.annotation.Transactional)
:匹配带有Transactional注解的方法。args(java.io.Serializable)
:匹配接受Serializable类型参数的方法。
切点表达式可以组合使用,以实现更复杂的匹配规则:
&&
:逻辑与,两者都匹配时才匹配。||
:逻辑或,其中之一匹配时就匹配。!
:逻辑非,取反操作。
示例:
如果你想要匹配com.example.service包下所有类的所有public方法,并且这些方法上有@Transactional注解,你可以使用以下切点表达式:
@Pointcut("execution(public * com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalService() {
// 方法体可以为空,这里只是作为切点的一个标识
}
然后你可以在通知中引用这个切点:
@Before("transactionalService()")
public void beforeTransactionalMethod(JoinPoint joinPoint) {
// 在事务性方法执行之前做一些事情
}
七、Spring AOP 通知
每个通知都可以通过@Before、@AfterReturning、@AfterThrowing、@After和@Around注解来声明,并且可以通过pointcut属性或者作为方法参数来指定要应用的切点。以下是一些使用通知的示例:
// 前置通知
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
System.out.println("执行前置通知");
}
// 后置通知
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(Object result) {
System.out.println("执行后置通知,返回值: " + result);
}
// 异常通知
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowingAdvice(Exception ex) {
System.out.println("执行异常通知,异常信息: " + ex.getMessage());
}
// 最终通知
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice() {
System.out.println("执行最终通知");
}
// 环绕通知
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("执行环绕通知之前");
Object result = pjp.proceed(); // 调用目标方法
System.out.println("执行环绕通知之后");
return result;
}
八、Spring AOP 失效场景
Spring AOP(面向切面编程)可能在以下场景中失效:
- 直接实例化对象:如果通过new关键字直接实例化一个类,而不是通过Spring容器获取,那么这个实例就不会被Spring的代理机制所管理,因此AOP的功能也就无法应用。
- 非Spring管理的Bean:如果Bean没有被Spring容器管理,即没有使用Spring的注解或XML配置,那么AOP无法对其应用。
- final方法或类:Spring AOP无法代理final方法,因为final方法不能被子类覆盖,也就无法对这些方法应用通知。同样,如果一个类被声明为final,那么也不能使用基于类的代理。
- 基于接口的JDK动态代理限制:如果目标对象没有实现接口,Spring将无法使用JDK动态代理,需要使用CGLIB代理,但这也有局限性,因为CGLIB代理主要是靠继承的方式动态生成目标类的子类来实现,所以无法代理final类。
- 私有方法:默认情况下,Spring AOP不会拦截私有方法。尽管可以通过特定配置使CGLIB代理拦截私有方法,但这并不是一个好的实践。
- 构造函数:构造函数不能被代理,因为它们是用来初始化对象状态的,而且在对象创建时就会被调用。
- 静态方法:静态方法属于类本身,而不是类的实例,因此Spring AOP无法对静态方法应用通知。
- 同类型的方法调用:如果一个Spring管理的Bean内部调用同一个Bean的另一个方法,这种自调用不会经过代理,因此不会应用AOP通知。
- 并发模式:在使用某些并发模式时(如CallerRunsPolicy、SameThreadExecution),可能会绕过代理,导致AOP不被触发。
九、Spring AOP使用实例
- 在Spring Boot中使用AOP,首先需要在项目中添加AOP相关的依赖。可以在pom.xml文件中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 接下来再定义一个切面类,使用@Aspect注解标记。然后,定义切点表达式来指定哪些方法需要被拦截,以及通知(Advice)。
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("方法执行之前:" + joinPoint.getSignature().getName());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("方法执行之后:" + joinPoint.getSignature().getName());
}
}
在上面的例子中,@Before注解定义了一个前置通知,它会在com.example.service包下所有方法执行之前被调用。@After注解定义了一个后置通知,它会在这些方法执行之后被调用。JoinPoint参数提供了关于正在执行的方法的信息。
切点表达式execution(* com.example.service.*.*(..))
的含义是:
execution
:指定这是一个执行通知。* com.example.service.*
:匹配com.example.service包下的所有类。*(..)
:匹配任意方法,无论参数数量和类型。
- 最后在Spring Boot应用的主类上添加
@EnableAspectJAutoProxy
注解,以启用自动代理,使得AOP切面能够生效:
@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringBootAopApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootAopApplication.class, args);
}
}
当运行Spring Boot应用,并且调用com.example.service包下的任何方法时,都会触发LoggingAspect中定义的前置和后置通知,实现日志记录的功能。