一、Spring AOP 底层实现揭秘
(一)动态代理:AOP 的核心技术
- JDK 动态代理:依赖接口实现代理,通过 InvocationHandler 接口和 Proxy 类动态创建代理对象,在运行期对目标对象方法进行增强。
- JDK 动态代理是基于反射实现的。它需要两个组件:InvocationHandler 接口和 Proxy 类。在使用 JDK 动态代理时,需要编写一个类实现 InvocationHandler 接口,并重写 invoke 方法,这个方法就是提供的代理方法。然后通过 Proxy 类的 newProxyInstance 方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理。当通过代理对象调用这些方法时,底层将通过反射,调用实现的 invoke 方法。
- JDK 动态代理的优点包括:是 JDK 原生的,不需要任何依赖即可使用;通过反射机制生成代理类的速度要比 CGLib 操作字节码生成代理类的速度更快。
- 缺点有:如果要使用 JDK 动态代理,被代理的类必须实现了接口,否则无法代理;JDK 动态代理无法为没有在接口中定义的方法实现代理;JDK 动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低。
- CGLIB 动态代理:采用字节码技术,为目标类创建子类,通过拦截父类方法调用实现增强,不依赖接口。
- CGLib 实现动态代理的原理是,底层采用了 ASM 字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将额外的逻辑(如 Spring 中的切面)织入到方法中,对方法进行了增强。
- CGLib 动态代理的优点是:使用 CGLib 代理的类,不需要实现接口,因为 CGLib 生成的代理类是直接继承自需要被代理的类。
(二)静态代理与动态代理对比
- 静态代理的缺点:
- 接口增多时代理类也增多。因为静态代理代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object。如果项目中存在多个接口,那么就会产生多个对应的实现类,代理类也会随着实现类的增加而增加。
- 功能增加时代理类维护困难。随着接口中的功能的增加,实现类中的功能也会同步增加,代理类中的增强方法也会增加。
- 增强内容复用性不彻底。从严格意义上来讲,定义的增强的内容(事务处理)没有达到彻底复用。
- 动态代理的优势:
- 运行期创建代理对象,不修改源代码实现方法增强。动态代理可以在程序运行期间,根据需要创建代理对象,对目标对象的方法进行增强,而不需要修改目标对象的源代码。无论是 JDK 动态代理还是 CGLib 动态代理,都可以在不改变原有代码的基础上,实现对方法的增强,提高了代码的可维护性和可扩展性。
二、Spring AOP 高级配置详解
(一)XML 模式配置 AOP
- 引入相关依赖,如 spring-aop 和 aspectjweaver。
- 在使用 Spring AOP 的 XML 模式配置时,首先需要在项目中引入必要的依赖。其中,spring-aop提供了 Spring AOP 的核心功能,而aspectjweaver则是 AspectJ 的编织器,用于在运行时将切面织入到目标对象中。
- 通过 Maven 或 Gradle 等构建工具,可以方便地将这些依赖添加到项目中。
- 在 Spring 配置文件中进行 AOP 配置,包括把通知 Bean 交给 Spring 管理、使用aop:config开始配置、配置切面和通知类型。
- 在 Spring 的 XML 配置文件中,首先需要将通知类定义为 Bean,以便 Spring 能够管理它们。例如:
<bean id="myAdvice" class="com.example.MyAdvice"/>
- 然后,使用<aop:config>标签开始 AOP 配置。在<aop:config>内部,可以配置切面和通知类型。例如:
<aop:config>
<aop:aspect ref="myAdvice">
<!-- 配置通知类型 -->
</aop:aspect>
</aop:config>
- 切入点表达式的概念和用法,如全限定法名、访问修饰符、返回值、包名、类名、方法名和参数列表的表示方法。
- 切入点表达式是用于指定在哪些连接点上应用切面的表达式。它可以通过多种方式来表示,包括全限定法名、访问修饰符、返回值、包名、类名、方法名和参数列表等。
- 例如,execution(* com.example.service.*.*(..))表示匹配com.example.service包下任意类的任意方法。其中,*表示任意返回值,com.example.service是包名,*是类名,*是方法名,(..)表示任意参数列表。
- 访问修饰符可以在表达式中指定,例如public表示只匹配公共方法。返回值类型也可以指定,如void表示只匹配返回值为void的方法。
- 改变代理方式的配置,可通过aop:config或aop:aspectj-autoproxy标签强制使用基于子类的动态代理(cglib 方式)。
- 在 Spring AOP 中,默认情况下会根据目标对象的情况选择合适的代理方式。如果目标对象实现了接口,则会使用基于接口的 JDK 动态代理;如果目标对象没有实现接口,则会自动切换到基于子类的动态代理(cglib 方式)。
- 但是,有时候我们可能需要强制使用基于子类的动态代理,可以通过在配置文件中设置proxy-target-class="true"来实现。例如:
<aop:config proxy-target-class="true">
<!-- AOP 配置 -->
</aop:config>
- 或者使用<aop:aspectj-autoproxy proxy-target-class="true">标签来开启基于子类的动态代理。
(二)XML + 注解模式配置 AOP
- 使用aop:aspectj-autoproxy标签开启注解配置 AOP 的支持。
- 在 Spring 的 XML 配置文件中,可以使用<aop:aspectj-autoproxy>标签来开启对注解配置 AOP 的支持。
<aop:aspectj-autoproxy/>
- 这样,Spring 容器在启动时会扫描带有特定注解的类,并将其识别为切面类,自动应用 AOP 功能。
- 通过注解定义切面、通知方法和切入点表达式。
- 在使用 XML + 注解模式配置 AOP 时,可以通过注解来定义切面、通知方法和切入点表达式。
- 首先,在切面类上使用@Aspect注解来标识该类是一个切面类。例如:
@Aspect
public class MyAspect {
//...
}
- 然后,在通知方法上使用相应的注解来定义通知类型,如@Before表示前置通知、@AfterReturning表示后置通知、@AfterThrowing表示异常通知、@After表示最终通知、@Around表示环绕通知。例如:
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
// 前置通知逻辑
}
- 在通知方法的注解中,可以通过切入点表达式来指定在哪些连接点上应用该通知。切入点表达式的写法与 XML 模式配置中的切入点表达式相同。
(三)注解模式配置 AOP
- 使用特定注解(如@Aspect)标记切面类。
- 在注解模式配置 AOP 中,首先需要使用@Aspect注解来标记切面类。
@Aspect
public class MyAspect {
//...
}
- 这样,Spring 容器在启动时会自动识别带有@Aspect注解的类,并将其作为切面类进行处理。
- 在切面类中使用注解定义通知方法(如@Before、@AfterReturning、@AfterThrowing、@After、@Around)和切入点表达式。
- 在切面类中,可以使用特定的注解来定义通知方法和切入点表达式。
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
// 前置通知逻辑
}
- 使用@AfterReturning注解定义后置通知方法:
@AfterReturning("execution(* com.example.service.*.*(..))")
public void afterReturningAdvice() {
// 后置通知逻辑
}
- 使用@AfterThrowing注解定义异常通知方法:
@AfterThrowing("execution(* com.example.service.*.*(..))")
public void afterThrowingAdvice(Throwable ex) {
// 异常通知逻辑
}
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice() {
// 最终通知逻辑
}
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
// 环绕通知逻辑
return pjp.proceed();
}
- 在这些通知方法的注解中,可以通过切入点表达式来指定在哪些连接点上应用该通知。切入点表达式的写法与 XML 模式配置中的切入点表达式相同。
三、优化 Spring AOP 面向切面编程
(一)减少代码冗余
- 通过动态代理让程序员只关注核心代码,避免静态代理带来的多个代理类和代码冗余问题。
- Spring AOP 利用动态代理技术,使得程序员在开发过程中无需关注代理类的生成和管理,只需专注于业务逻辑的核心代码。与静态代理相比,动态代理在运行时根据需要自动创建代理对象,避免了为每个被代理对象编写特定的代理类,从而减少了代码的冗余。
- 例如,在日志处理、事务管理等场景中,使用动态代理可以将这些通用的功能从业务代码中分离出来,通过切面的方式统一管理,提高了代码的可维护性和可扩展性。
- 提取公共功能到增强处理类中,如日志处理、事务处理、异常处理、性能分析等。
- 将公共功能提取到增强处理类中是优化 Spring AOP 的重要手段之一。以日志处理为例,可以在增强处理类中定义一个前置通知方法,在目标方法执行前记录日志信息。同样,事务处理可以通过环绕通知来确保事务的正确开启、提交或回滚。异常处理可以在异常通知中捕获并处理异常,避免异常在系统中扩散。性能分析可以通过记录方法执行的开始时间和结束时间,计算方法的执行时间,从而优化系统性能。
- 例如,在一个电商系统中,可以将订单处理的日志记录、事务管理、异常处理等功能提取到一个增强处理类中,当订单处理的业务方法被调用时,Spring AOP 会自动应用这些增强功能,无需在每个业务方法中重复编写这些代码。
(二)合理选择代理方式
- 根据被代理对象的实际情况选择合适的代理方式,如被代理对象实现接口时可使用 JDK 动态代理,否则可考虑 CGLIB 动态代理。
- 在 Spring AOP 中,代理方式的选择取决于被代理对象的具体情况。如果被代理对象实现了接口,那么可以使用 JDK 动态代理。JDK 动态代理基于接口实现代理,通过InvocationHandler接口和Proxy类动态创建代理对象。在运行期对目标对象方法进行增强时,通过反射机制调用目标方法。
- 例如,一个服务类实现了某个业务接口,在这种情况下,使用 JDK 动态代理可以方便地对服务类的方法进行增强,而无需修改服务类的代码。
- 如果被代理对象没有实现接口,则可考虑使用 CGLIB 动态代理。CGLIB 采用字节码技术,为目标类创建子类,通过拦截父类方法调用实现增强。CGLIB 动态代理在运行时创建目标类的子类,并重写目标类的所有可重写方法,在方法调用时插入增强逻辑。
- 例如,一个没有实现接口的工具类,需要对其方法进行增强时,可以使用 CGLIB 动态代理。
- 可通过配置强制使用基于子类的动态代理(cglib 方式),提高灵活性。
- 在 Spring AOP 中,可以通过配置强制使用基于子类的动态代理(CGLIB 方式)。在 XML 配置中,可以通过设置<aop:config proxy-target-class="true">来实现。或者在使用注解配置时,可以通过设置@EnableAspectJAutoProxy(proxyTargetClass = true)来开启基于子类的动态代理。
- 强制使用 CGLIB 动态代理可以提高灵活性,尤其是在一些特殊情况下,如目标类没有实现接口,或者需要对没有接口的类进行增强时。同时,CGLIB 动态代理可以对目标类的所有方法进行增强,包括私有方法和最终方法,而 JDK 动态代理只能对实现了接口的方法进行增强。
- 例如,在一个遗留系统中,有一些没有实现接口的类需要进行日志记录和性能分析等增强操作。通过强制使用 CGLIB 动态代理,可以方便地对这些类进行增强,而无需修改这些类的代码。
(三)优化通知配置
- 合理设置前置通知、后置通知、异常通知、最终通知和环绕通知,根据业务需求选择合适的通知类型。
- 在 Spring AOP 中,有五种通知类型可供选择:前置通知(@Before)、后置通知(@After)、异常通知(@AfterThrowing)、最终通知(@After)和环绕通知(@Around)。合理设置这些通知类型可以根据业务需求在不同的阶段对目标方法进行增强。
- 前置通知在目标方法执行前执行,可以用于参数验证、日志记录等操作。后置通知在目标方法正常执行后执行,可以用于清理资源、记录方法执行时间等操作。异常通知在目标方法抛出异常时执行,可以用于记录异常信息、进行异常处理等操作。最终通知无论目标方法是否抛出异常都会执行,可以用于释放资源、记录方法执行结果等操作。环绕通知可以在目标方法执行前后自定义行为,是最强大的通知类型,可以用于控制方法的执行流程、记录方法执行时间、处理异常等操作。
- 例如,在一个用户注册的业务场景中,可以使用前置通知进行参数验证,确保用户输入的信息合法;使用后置通知记录用户注册的时间;使用异常通知记录注册过程中出现的异常信息;使用最终通知清理注册过程中使用的临时资源;使用环绕通知控制注册方法的执行流程,确保注册过程的完整性。
- 优化切入点表达式的写法,提高准确性和可读性。
- 切入点表达式是用于指定在哪些连接点上应用切面的表达式。优化切入点表达式的写法可以提高准确性和可读性,减少错误的发生。
- 切入点表达式可以通过多种方式来表示,包括全限定法名、访问修饰符、返回值、包名、类名、方法名和参数列表等。在编写切入点表达式时,应该尽量使用明确的包名、类名和方法名,避免使用通配符(*)过多,以免影响表达式的准确性。同时,可以使用一些命名约定来提高表达式的可读性,例如使用有意义的方法名和参数名。
- 例如,execution(* com.example.service.UserService.register(..))表示匹配com.example.service.UserService类中的register方法。这个表达式明确指定了包名、类名和方法名,提高了准确性和可读性。
(四)考虑对获取 Bean 的影响
- 理解 Spring AOP 对获取 Bean 的影响,避免因 AOP 配置不当导致的问题。
- Spring AOP 对获取 Bean 有一定的影响。当使用 AOP 对一个类进行增强时,实际上是在 Spring 容器中创建了一个代理对象。这个代理对象包含了目标对象的所有方法和属性,同时在指定的连接点上插入了切面的通知。
- 如果目标类实现了接口,并且在 AOP 配置中使用了基于接口的代理方式(JDK 动态代理),那么在获取 Bean 时,可以通过接口类型来获取代理对象。如果通过目标类的类型来获取 Bean,可能会导致获取不到对象或者获取到错误的对象。
- 如果目标类没有实现接口,并且在 AOP 配置中使用了基于子类的代理方式(CGLIB 动态代理),那么在获取 Bean 时,可以通过目标类的类型来获取代理对象。但是,如果目标类有多个子类,并且在 AOP 配置中没有明确指定代理的目标类,可能会导致获取到错误的对象。
- 例如,在一个用户服务的业务场景中,有一个UserService接口和一个UserServiceImpl实现类。如果对UserServiceImpl类进行了 AOP 增强,并且在 AOP 配置中使用了基于接口的代理方式,那么在获取 Bean 时,应该通过UserService接口类型来获取代理对象。如果通过UserServiceImpl类的类型来获取 Bean,可能会导致获取不到对象或者获取到错误的对象。
- 合理设置切面优先级,确保通知的执行顺序符合业务需求。
- 在 Spring AOP 中,可以使用@Order注解来设置切面的优先级。值越小,优先级越高。合理设置切面优先级可以确保通知的执行顺序符合业务需求。
- 例如,在一个日志记录和事务管理的业务场景中,可能需要先记录日志,然后再进行事务管理。在这种情况下,可以将日志记录切面的优先级设置为较高的值,将事务管理切面的优先级设置为较低的值。这样,在方法执行时,先执行日志记录切面的通知,然后再执行事务管理切面的通知。
- 另外,还可以通过在通知方法中使用@AfterReturning、@AfterThrowing等注解来指定通知的执行顺序。例如,在一个异常处理的业务场景中,可以使用@AfterThrowing注解来指定在方法抛出异常时执行的通知,然后在这个通知方法中进行异常处理,并记录异常信息。这样,可以确保异常处理
本文暂时没有评论,来添加一个吧(●'◡'●)