14-Spring(2)
1. 动态代理#
1.1 代理模式的原理#
使用一个代理将原本对象包装起来,然后用该代理对象”取代”原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
1.2 动态代理的方式#
- 基于接口实现动态代理: JDK 动态代理(代理对象和和被代理对象实现相同的接口)
- 基于继承实现动态代理: Cglib、Javassist 动态代理
2. AOP#
2.1 概述#
AOP(Aspect-Oriented Programming) 是一种新的方法论,是对传统 OOP(Object-Oriented Programming) 的补充。
面向对象 → 纵向继承机制
面向切面 → 横向抽取机制
AOP 编程操作的主要对象是 [切面 (存放公共功能的类)],而切面用于模块化 [横切关注点 (公共功能)]。
在应用 AOP 编程时,仍然需要定义公共功能,但可以明确地定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来 [横切关注点] 就被模块化到特殊的类里 —— 这样的类我们通常称之为 "切面"。
AOP 的好处:① 每个事物逻辑位于一个位置,代码不分散,便于维护和升级;② 业务模块更简洁,只包含核心业务代码
2.2 AOP 术语#
- 横切关注点:从每个方法中抽取出来的同一类非核心业务
- 切面 (Aspect):封装 [横切关注点] 信息的类,每个关注点体现为一个 [通知] 方法
- 通知 (Advice):[切面] 必须要完成的各个具体工作;[通知] 封装了 [横切关注点]
- 目标 (Target):被通知的对象
- 代理 (Proxy):向目标对象应用 [通知] 之后创建的代理对象
- 连接点 (Joinpoint):[横切关注点] 在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。简单来讲,[横切关注点] 抽取出来的位置就叫 [连接点]。
- 切入点(Pointcut):定位 [连接点] 的条件,是一个表达式。每个类的方法中都包含多个 [连接点],所以 [连接点] 是类中客观存在的事物。如果把 [连接点] 看作数据库中的记录,那么 [切入点表达式] 就是查询条件。AOP 可以通过 [切入点表达式] 定位到 [切入点(特定的连接点)]。[切入点表达式] 通过
org.springframework.aop.Pointcut<I>
进行描述,它使用类和方法作为 [连接点] 的查询条件。
- [横切关注点] 和 [通知] 这俩是一个意思,只是代码所处位置不同而产生的不同的叫法:
- 横切关注点:从业务方法中抽取出来的同一类非核心业务
- 通知:把横切关注点封装到一个方法放在切面类中时,称它为“通知”。
- [切入点] 和 [切入点表达式]
- [切入点] 侧重于方法中的哪个位置
- [切入点表达式] 侧重于是哪个方法
2.3. AspectJ#
AspectJ 是 Java 社区里最完整最流行的 AOP 框架。在 Spring 2.0 以上版本中,可以使用基于 AspectJ 注解或基于 XML 配置的 AOP。AspectJ 是 AOP 思想的具体实现。
在 Spring 中启用 AspectJ 注解支持:
- 导入 jar 包
- 引入 aop 名称空间
- 配置:当 Spring IOC 容器侦测到 bean 配置文件中的
<aop:aspectj-autoproxy/>
元素时,会自动为与 AspectJ 切面匹配的 bean 创建代理
用 AspectJ 注解声明切面:
- 要在 Spring 中声明 AspectJ 切面,只需要在 IOC 容器中将切面声明为 bean 实例
- 当在 Spring IOC 容器中初始化 AspectJ 切面之后,Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 bean 创建代理
- 在 AspectJ 注解中,切面只是一个带有 @Aspect 注解的 Java 类,它往往要包含很多 [通知]
- 通知是标注有某种注解的简单的 Java 方法,AspectJ 支持 5 种类型的通知注解(其必设属性 value 的值为切入点表达式)
- @Before:前置通知,在方法执行之前执行
- @After:后置通知,在方法执行之后执行
- @AfterReturning:返回通知,在方法返回结果之后执行
- @AfterThrowing:异常通知,在方法抛出异常之后执行
- @Around:环绕通知,围绕着方法执行
3. AOP 细节#
3.1 切入点表达式#
[作用] 通过表达式的方式定位一个或多个具体的连接点。
[语法格式] execution([权限修饰符] [返回值类型] [简单类名/全类名] 方法名)
举例说明:
切入点表达式应用到实际的切面类中:
3.2 JoinPoint#
切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在 JoinPoint<I>
的实例对象中。
@Before("execution(* cn.edu.nuist.spring.aop.*.*(..))")
// JoinPoint 参数的值是由框架赋予,必须是第一个位置的参数
public void beforeMethod(JoinPoint joinPoint) {
// 获取方法的参数
Object[] args = joinPoint.getArgs();
// 方法签名
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
System.out.println("method: " + methodName + ", args: " + Arrays.toString(args));
System.out.println("方法执行之前");
}
3.3 必须被容器管理#
无论是切面类,还是目标类,都必须在 Spring 容器的管理下才能有效。所以切面类上不仅要加上 @Aspect,还得加 @Component,使切面和 SpringIOC 容器产生关系,让其作为一个组件。目标类同理。
3.4 通知#
- 在具体的连接点上要执行的操作
- 一个切面可以包括一个或者多个通知
- 通知所使用的注解的值往往是切入点表达式
3.4.1 前置通知#
- 前置通知是在方法执行之前执行的通知
- 使用 @Before 注解
3.4.2 后置通知#
- 后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候执行的通知
- 使用 @After 注解
3.4.3 返回通知#
- 无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
- 使用 @AfterReturning 注解,可在返回通知中访问连接点的返回值
- 在返回通知中,只要将 returning 属性添加到 @AfterReturning 注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称
- 必须在通知方法的签名中添加一个同名参数。在运行时 Spring AOP 会通过这个参数传递返回值
- 举例说明
@AfterReturning(value="execution(* cn.edu.nuist.spring.aop.*.*(..))", returning="result") public void afterReturning(JoinPoint joinPoint, Object result) { System.out.println("[返回通知] " + joinPoint.getSignature().getName() + "方法的返回值为:" + result); }
3.4.4 异常通知#
- 异常通知是只在连接点抛出异常时才执行的通知
- 将 throwing 属性添加到 @AfterThrowing 注解中,也可以访问连接点抛出的异常。Throwable 是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常
- 如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行
- 举例说明
@AfterThrowing(value="execution(* cn.edu.nuist.spring.aop.*.*(..))", throwing = "e") public void afterThrowing(NullPointerException e) { System.out.println("[异常通知] " + e); }
3.4.5 环绕通知#
- 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点
- 对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint。它是 JoinPoint 的子接口,允许控制何时执行,是否执行连接点
- 在环绕通知中需要明确调用 ProceedingJoinPoint 的
proceed()
来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行 - 环绕通知的方法需要返回目标方法执行之后的结果,即调用
joinPoint.proceed()
的返回值,否则会出现 NullPointerException
@Around("execution(* cn.edu.nuist.spring.aop.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint pjp) {
Object result = null;
try {
System.out.println("前置通知");
result = pjp.proceed(); // 执行方法
System.out.println("返回通知");
return result;
} catch (Throwable e) {
e.printStackTrace();
System.out.println("异常通知");
} finally {
System.out.println("后置通知");
}
return null;
}
3.5 重用切入点#
- 在编写 AspectJ 切面时,可以直接在通知注解中书写切入点表达式。但同一个切入点表达式可能会在多个通知中重复出现
- 在 AspectJ 切面中,可以通过 @Pointcut 注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的
- 切入点方法的访问控制符同时在控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它必须被声明为 public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
- 其他通知可以通过方法名称引入该切入点
3.6 指定切面的优先级#
- 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的
- 切面的优先级可以通过实现
Ordered<I>
或利用 @Order 注解指定 - 实现
Ordered<I>
,getOrder()
的返回值越小,优先级越高 - 若使用 @Order 注解,序号出现在注解中
4. 以 XML 方式配置切面#
4.1 概述#
除了使用 AspectJ 注解声明切面,Spring 也支持在 bean 配置文件中声明切面。这种声明是通过 AOP 命名空间中的 XML 元素完成的。
正常情况下,基于注解的声明要优先于基于 XML 的声明。通过 AspectJ 注解,切面可以与 AspectJ 兼容,而基于 XML 的配置则是 Spring 专有的。由于 AspectJ 得到越来越多的 AOP 框架支持,所以,以注解风格编写的切面将会有更多重用的机会。
4.2 配置细节#
在 bean 配置文件中,所有的 Spring AOP 配置都必须定义在 <aop:config>
元素内部。对于每个切面而言,都要创建一个 <aop:aspect>
元素来为具体的切面实现引用后端 bean 实例。
切面 bean 必须有一个标识符,供 <aop:aspect>
元素引用。
4.3 声明切入点#
- 切入点使用
<aop:pointcut>
元素声明 - 切入点必须定义在
<aop:aspect>
元素下,或者直接定义在<aop:config>
元素下- 定义在
<aop:aspect>
元素下:只对当前切面有效 - 定义在
<aop:config>
元素下:对所有切面都有效
- 定义在
- 基于 XML 的 AOP 配置不允许在切入点表达式中用名称引入其他切入点
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?