百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

掌握Spring AOP的底层实现与高级配置,优化面向切面编程

haoteby 2024-12-29 03:21 10 浏览

一、Spring AOP 底层实现揭秘

(一)动态代理:AOP 的核心技术

  1. JDK 动态代理:依赖接口实现代理,通过 InvocationHandler 接口和 Proxy 类动态创建代理对象,在运行期对目标对象方法进行增强。
    • JDK 动态代理是基于反射实现的。它需要两个组件:InvocationHandler 接口和 Proxy 类。在使用 JDK 动态代理时,需要编写一个类实现 InvocationHandler 接口,并重写 invoke 方法,这个方法就是提供的代理方法。然后通过 Proxy 类的 newProxyInstance 方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理。当通过代理对象调用这些方法时,底层将通过反射,调用实现的 invoke 方法。
    • JDK 动态代理的优点包括:是 JDK 原生的,不需要任何依赖即可使用;通过反射机制生成代理类的速度要比 CGLib 操作字节码生成代理类的速度更快。
    • 缺点有:如果要使用 JDK 动态代理,被代理的类必须实现了接口,否则无法代理;JDK 动态代理无法为没有在接口中定义的方法实现代理;JDK 动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低。
  1. CGLIB 动态代理:采用字节码技术,为目标类创建子类,通过拦截父类方法调用实现增强,不依赖接口。
    • CGLib 实现动态代理的原理是,底层采用了 ASM 字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将额外的逻辑(如 Spring 中的切面)织入到方法中,对方法进行了增强。
    • CGLib 动态代理的优点是:使用 CGLib 代理的类,不需要实现接口,因为 CGLib 生成的代理类是直接继承自需要被代理的类。

(二)静态代理与动态代理对比

  1. 静态代理的缺点:
    • 接口增多时代理类也增多。因为静态代理代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object。如果项目中存在多个接口,那么就会产生多个对应的实现类,代理类也会随着实现类的增加而增加。
    • 功能增加时代理类维护困难。随着接口中的功能的增加,实现类中的功能也会同步增加,代理类中的增强方法也会增加。
    • 增强内容复用性不彻底。从严格意义上来讲,定义的增强的内容(事务处理)没有达到彻底复用。
  1. 动态代理的优势:
    • 运行期创建代理对象,不修改源代码实现方法增强。动态代理可以在程序运行期间,根据需要创建代理对象,对目标对象的方法进行增强,而不需要修改目标对象的源代码。无论是 JDK 动态代理还是 CGLib 动态代理,都可以在不改变原有代码的基础上,实现对方法的增强,提高了代码的可维护性和可扩展性。

二、Spring AOP 高级配置详解

(一)XML 模式配置 AOP

  1. 引入相关依赖,如 spring-aop 和 aspectjweaver。
    • 在使用 Spring AOP 的 XML 模式配置时,首先需要在项目中引入必要的依赖。其中,spring-aop提供了 Spring AOP 的核心功能,而aspectjweaver则是 AspectJ 的编织器,用于在运行时将切面织入到目标对象中。
    • 通过 Maven 或 Gradle 等构建工具,可以方便地将这些依赖添加到项目中。
  1. 在 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>
  1. 切入点表达式的概念和用法,如全限定法名、访问修饰符、返回值、包名、类名、方法名和参数列表的表示方法。
    • 切入点表达式是用于指定在哪些连接点上应用切面的表达式。它可以通过多种方式来表示,包括全限定法名、访问修饰符、返回值、包名、类名、方法名和参数列表等。
    • 例如,execution(* com.example.service.*.*(..))表示匹配com.example.service包下任意类的任意方法。其中,*表示任意返回值,com.example.service是包名,*是类名,*是方法名,(..)表示任意参数列表。
    • 访问修饰符可以在表达式中指定,例如public表示只匹配公共方法。返回值类型也可以指定,如void表示只匹配返回值为void的方法。
  1. 改变代理方式的配置,可通过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

  1. 使用aop:aspectj-autoproxy标签开启注解配置 AOP 的支持。
    • 在 Spring 的 XML 配置文件中,可以使用<aop:aspectj-autoproxy>标签来开启对注解配置 AOP 的支持。
    • 例如:
<aop:aspectj-autoproxy/>
  • 这样,Spring 容器在启动时会扫描带有特定注解的类,并将其识别为切面类,自动应用 AOP 功能。
  1. 通过注解定义切面、通知方法和切入点表达式。
    • 在使用 XML + 注解模式配置 AOP 时,可以通过注解来定义切面、通知方法和切入点表达式。
    • 首先,在切面类上使用@Aspect注解来标识该类是一个切面类。例如:
@Aspect
public class MyAspect {
 //...
}
  • 然后,在通知方法上使用相应的注解来定义通知类型,如@Before表示前置通知、@AfterReturning表示后置通知、@AfterThrowing表示异常通知、@After表示最终通知、@Around表示环绕通知。例如:
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
 // 前置通知逻辑
}
  • 在通知方法的注解中,可以通过切入点表达式来指定在哪些连接点上应用该通知。切入点表达式的写法与 XML 模式配置中的切入点表达式相同。

(三)注解模式配置 AOP

  1. 使用特定注解(如@Aspect)标记切面类。
    • 在注解模式配置 AOP 中,首先需要使用@Aspect注解来标记切面类。
    • 例如:
@Aspect
public class MyAspect {
 //...
}
  • 这样,Spring 容器在启动时会自动识别带有@Aspect注解的类,并将其作为切面类进行处理。
  1. 在切面类中使用注解定义通知方法(如@Before、@AfterReturning、@AfterThrowing、@After、@Around)和切入点表达式。
    • 在切面类中,可以使用特定的注解来定义通知方法和切入点表达式。
    • 例如,使用@Before注解定义前置通知方法:
@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注解定义最终通知方法:
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice() {
 // 最终通知逻辑
}
  • 使用@Around注解定义环绕通知方法:
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
 // 环绕通知逻辑
 return pjp.proceed();
}
  • 在这些通知方法的注解中,可以通过切入点表达式来指定在哪些连接点上应用该通知。切入点表达式的写法与 XML 模式配置中的切入点表达式相同。

三、优化 Spring AOP 面向切面编程

(一)减少代码冗余

  1. 通过动态代理让程序员只关注核心代码,避免静态代理带来的多个代理类和代码冗余问题。
    • Spring AOP 利用动态代理技术,使得程序员在开发过程中无需关注代理类的生成和管理,只需专注于业务逻辑的核心代码。与静态代理相比,动态代理在运行时根据需要自动创建代理对象,避免了为每个被代理对象编写特定的代理类,从而减少了代码的冗余。
    • 例如,在日志处理、事务管理等场景中,使用动态代理可以将这些通用的功能从业务代码中分离出来,通过切面的方式统一管理,提高了代码的可维护性和可扩展性。
  1. 提取公共功能到增强处理类中,如日志处理、事务处理、异常处理、性能分析等。
    • 将公共功能提取到增强处理类中是优化 Spring AOP 的重要手段之一。以日志处理为例,可以在增强处理类中定义一个前置通知方法,在目标方法执行前记录日志信息。同样,事务处理可以通过环绕通知来确保事务的正确开启、提交或回滚。异常处理可以在异常通知中捕获并处理异常,避免异常在系统中扩散。性能分析可以通过记录方法执行的开始时间和结束时间,计算方法的执行时间,从而优化系统性能。
    • 例如,在一个电商系统中,可以将订单处理的日志记录、事务管理、异常处理等功能提取到一个增强处理类中,当订单处理的业务方法被调用时,Spring AOP 会自动应用这些增强功能,无需在每个业务方法中重复编写这些代码。

(二)合理选择代理方式

  1. 根据被代理对象的实际情况选择合适的代理方式,如被代理对象实现接口时可使用 JDK 动态代理,否则可考虑 CGLIB 动态代理。
    • 在 Spring AOP 中,代理方式的选择取决于被代理对象的具体情况。如果被代理对象实现了接口,那么可以使用 JDK 动态代理。JDK 动态代理基于接口实现代理,通过InvocationHandler接口和Proxy类动态创建代理对象。在运行期对目标对象方法进行增强时,通过反射机制调用目标方法。
    • 例如,一个服务类实现了某个业务接口,在这种情况下,使用 JDK 动态代理可以方便地对服务类的方法进行增强,而无需修改服务类的代码。
    • 如果被代理对象没有实现接口,则可考虑使用 CGLIB 动态代理。CGLIB 采用字节码技术,为目标类创建子类,通过拦截父类方法调用实现增强。CGLIB 动态代理在运行时创建目标类的子类,并重写目标类的所有可重写方法,在方法调用时插入增强逻辑。
    • 例如,一个没有实现接口的工具类,需要对其方法进行增强时,可以使用 CGLIB 动态代理。
  1. 可通过配置强制使用基于子类的动态代理(cglib 方式),提高灵活性。
    • 在 Spring AOP 中,可以通过配置强制使用基于子类的动态代理(CGLIB 方式)。在 XML 配置中,可以通过设置<aop:config proxy-target-class="true">来实现。或者在使用注解配置时,可以通过设置@EnableAspectJAutoProxy(proxyTargetClass = true)来开启基于子类的动态代理。
    • 强制使用 CGLIB 动态代理可以提高灵活性,尤其是在一些特殊情况下,如目标类没有实现接口,或者需要对没有接口的类进行增强时。同时,CGLIB 动态代理可以对目标类的所有方法进行增强,包括私有方法和最终方法,而 JDK 动态代理只能对实现了接口的方法进行增强。
    • 例如,在一个遗留系统中,有一些没有实现接口的类需要进行日志记录和性能分析等增强操作。通过强制使用 CGLIB 动态代理,可以方便地对这些类进行增强,而无需修改这些类的代码。

(三)优化通知配置

  1. 合理设置前置通知、后置通知、异常通知、最终通知和环绕通知,根据业务需求选择合适的通知类型。
    • 在 Spring AOP 中,有五种通知类型可供选择:前置通知(@Before)、后置通知(@After)、异常通知(@AfterThrowing)、最终通知(@After)和环绕通知(@Around)。合理设置这些通知类型可以根据业务需求在不同的阶段对目标方法进行增强。
    • 前置通知在目标方法执行前执行,可以用于参数验证、日志记录等操作。后置通知在目标方法正常执行后执行,可以用于清理资源、记录方法执行时间等操作。异常通知在目标方法抛出异常时执行,可以用于记录异常信息、进行异常处理等操作。最终通知无论目标方法是否抛出异常都会执行,可以用于释放资源、记录方法执行结果等操作。环绕通知可以在目标方法执行前后自定义行为,是最强大的通知类型,可以用于控制方法的执行流程、记录方法执行时间、处理异常等操作。
    • 例如,在一个用户注册的业务场景中,可以使用前置通知进行参数验证,确保用户输入的信息合法;使用后置通知记录用户注册的时间;使用异常通知记录注册过程中出现的异常信息;使用最终通知清理注册过程中使用的临时资源;使用环绕通知控制注册方法的执行流程,确保注册过程的完整性。
  1. 优化切入点表达式的写法,提高准确性和可读性。
    • 切入点表达式是用于指定在哪些连接点上应用切面的表达式。优化切入点表达式的写法可以提高准确性和可读性,减少错误的发生。
    • 切入点表达式可以通过多种方式来表示,包括全限定法名、访问修饰符、返回值、包名、类名、方法名和参数列表等。在编写切入点表达式时,应该尽量使用明确的包名、类名和方法名,避免使用通配符(*)过多,以免影响表达式的准确性。同时,可以使用一些命名约定来提高表达式的可读性,例如使用有意义的方法名和参数名。
    • 例如,execution(* com.example.service.UserService.register(..))表示匹配com.example.service.UserService类中的register方法。这个表达式明确指定了包名、类名和方法名,提高了准确性和可读性。

(四)考虑对获取 Bean 的影响

  1. 理解 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,可能会导致获取不到对象或者获取到错误的对象。
  1. 合理设置切面优先级,确保通知的执行顺序符合业务需求。
    • 在 Spring AOP 中,可以使用@Order注解来设置切面的优先级。值越小,优先级越高。合理设置切面优先级可以确保通知的执行顺序符合业务需求。
    • 例如,在一个日志记录和事务管理的业务场景中,可能需要先记录日志,然后再进行事务管理。在这种情况下,可以将日志记录切面的优先级设置为较高的值,将事务管理切面的优先级设置为较低的值。这样,在方法执行时,先执行日志记录切面的通知,然后再执行事务管理切面的通知。
    • 另外,还可以通过在通知方法中使用@AfterReturning、@AfterThrowing等注解来指定通知的执行顺序。例如,在一个异常处理的业务场景中,可以使用@AfterThrowing注解来指定在方法抛出异常时执行的通知,然后在这个通知方法中进行异常处理,并记录异常信息。这样,可以确保异常处理

相关推荐

一日一技:用Python程序将十进制转换为二进制

用Python程序将十进制转换为二进制通过将数字连续除以2并以相反顺序打印其余部分,将十进制数转换为二进制。在下面的程序中,我们将学习使用递归函数将十进制数转换为二进制数,代码如下:...

十进制转化成二进制你会吗?#数学思维

六年级奥赛起跑线:抽屉原理揭秘。同学们好,我是你们的奥耀老师。今天一起来学习奥赛起跑线第三讲二进制计数法。例一:把十进制五十三化成二进制数是多少?首先十进制就是满十进一,二进制就是满二进一。二进制每个...

二进制、十进制、八进制和十六进制,它们之间是如何转换的?

在学习进制时总会遇到多种进制转换的时候,学会它们之间的转换方法也是必须的,这里分享一下几种进制之间转换的方法,也分享两个好用的转换工具,使用它们能够大幅度的提升你的办公和学习效率,感兴趣的小伙伴记得点...

c语言-2进制转10进制_c语言 二进制转十进制

#include<stdio.h>intmain(){charch;inta=0;...

二进制、八进制、十进制和十六进制数制转换

一、数制1、什么是数制数制是计数进位的简称。也就是由低位向高位进位计数的方法。2、常用数制计算机中常用的数制有二进制、八进制、十进制和十六进制。...

二进制、十进制、八进制、十六进制间的相互转换函数

二进制、十进制、八进制、十六进制间的相互转换函数1、输入任意一个十进制的整数,将其分别转换为二进制、八进制、十六进制。2、程序代码如下:#include<iostream>usingna...

二进制、八进制、十进制和十六进制等常用数制及其相互转换

从大学开始系统的接触计算机专业,到现在已经过去十几年了,今天整理一下基础的进制转换,希望给还在上高中的表妹一个入门的引导,早日熟悉这个行业。一、二进制、八进制、十进制和十六进制是如何定义的?二进制是B...

二进制如何转换成十进制?_二进制如何转换成十进制例子图解

随着社会的发展,电器维修由继电器时代逐渐被PLC,变频器,触摸屏等工控时代所替代,特别是plc编程,其数据逻辑往往涉及到数制二进制,那么二进制到底是什么呢?它和十进制又有什么区别和联系呢?下面和朋友们...

二进制与十进制的相互转换_二进制和十进制之间转换

很多同学在刚开始接触计算机语言的时候,都会了解计算机的世界里面大多都是二进制来表达现实世界的任何事物的。当然现实世界的事务有很多很多,就拿最简单的数字,我们经常看到的数字大多都是十进制的形式,例如:我...

十进制如何转换为二进制,二进制如何转换为十进制

用十进制除以2,除的断的,商用0表示;除不断的,商用1表示余0时结束假如十进制用X表示,用十进制除以2,即x/2除以2后为整数的(除的断的),商用0表示;除以2除不断的,商用1表示除完后的商0或1...

十进制数如何转换为二进制数_十进制数如何转换为二进制数举例说明

我们经常听到十进制数和二进制数,电脑中也经常使用二进制数来进行计算,但是很多人却不清楚十进制数和二进制数是怎样进行转换的,下面就来看看,十进制数转换为二进制数的方法。正整数转二进制...

二进制转化为十进制,你会做吗?一起来试试吧

今天孩子问把二进制表示的110101改写成十进制数怎么做呀?,“二进制”简单来说就是“满二进一”,只用0和1共两个数字表示,同理我们平常接触到的“十进制”是“满十进一”,只用0-9共十个数字表示。如果...

Mac终于能正常打游戏了!苹果正逐渐淘汰Rosetta转译

Mac玩家苦转译久矣!WWDC2025苹果正式宣判Rosetta死刑,原生游戏时代终于杀到。Metal4光追和AI插帧技术直接掀桌,连Steam都连夜扛着ARM架构投诚了。看到《赛博朋克2077》...

怎么把视频的声音提出来转为音频?音频提取,11款工具实测搞定

想把视频里的声音单独保存为音频文件(MP3/AAC/WAV/FLAC)用于配音、播客、听课或二次剪辑?本文挑出10款常用工具,给出实测可复现的操作步骤、优缺点和场景推荐。1)转换猫mp3转换器(操作门...

6个mp4格式转换器测评:转换速度与质量并存!

MP4视频格式具有兼容性强、视频画质高清、文件体积较小、支持多种编码等特点,适用于网络媒体传播。如果大家想要将非MP4格式的视频转换成MP4的视频格式的话,可以使用MP4格式转换器更换格式。本文分别从...