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 的好处:① 每个事物逻辑位于一个位置,代码不分散,便于维护和升级;② 业务模块更简洁,只包含核心业务代码

AOP 图解:

2.2 AOP 术语#

  1. 横切关注点:从每个方法中抽取出来的同一类非核心业务
  2. 切面 (Aspect):封装 [横切关注点] 信息的类,每个关注点体现为一个 [通知] 方法
  3. 通知 (Advice):[切面] 必须要完成的各个具体工作;[通知] 封装了 [横切关注点]
  4. 目标 (Target):被通知的对象
  5. 代理 (Proxy):向目标对象应用 [通知] 之后创建的代理对象
  6. 连接点 (Joinpoint):[横切关注点] 在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。简单来讲,[横切关注点] 抽取出来的位置就叫 [连接点]。
  7. 切入点(Pointcut):定位 [连接点] 的条件,是一个表达式。每个类的方法中都包含多个 [连接点],所以 [连接点] 是类中客观存在的事物。如果把 [连接点] 看作数据库中的记录,那么 [切入点表达式] 就是查询条件。AOP 可以通过 [切入点表达式] 定位到 [切入点(特定的连接点)]。[切入点表达式] 通过 org.springframework.aop.Pointcut<I> 进行描述,它使用类和方法作为 [连接点] 的查询条件。
  • [横切关注点] 和 [通知] 这俩是一个意思,只是代码所处位置不同而产生的不同的叫法:
    • 横切关注点:从业务方法中抽取出来的同一类非核心业务
    • 通知:把横切关注点封装到一个方法放在切面类中时,称它为“通知”。
  • [切入点] 和 [切入点表达式]
    • [切入点] 侧重于方法中的哪个位置
    • [切入点表达式] 侧重于是哪个方法

2.3. AspectJ#

AspectJ 是 Java 社区里最完整最流行的 AOP 框架。在 Spring 2.0 以上版本中,可以使用基于 AspectJ 注解或基于 XML 配置的 AOP。AspectJ 是 AOP 思想的具体实现。

在 Spring 中启用 AspectJ 注解支持:

  1. 导入 jar 包
  2. 引入 aop 名称空间
  3. 配置:当 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 通知#

  1. 在具体的连接点上要执行的操作
  2. 一个切面可以包括一个或者多个通知
  3. 通知所使用的注解的值往往是切入点表达式

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 环绕通知#

  1. 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点
  2. 对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint。它是 JoinPoint 的子接口,允许控制何时执行,是否执行连接点
  3. 在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行
  4. 环绕通知的方法需要返回目标方法执行之后的结果,即调用 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 重用切入点#

  1. 在编写 AspectJ 切面时,可以直接在通知注解中书写切入点表达式。但同一个切入点表达式可能会在多个通知中重复出现
  2. 在 AspectJ 切面中,可以通过 @Pointcut 注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的
  3. 切入点方法的访问控制符同时在控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它必须被声明为 public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
  4. 其他通知可以通过方法名称引入该切入点

3.6 指定切面的优先级#

  1. 在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的
  2. 切面的优先级可以通过实现 Ordered<I> 或利用 @Order 注解指定
  3. 实现 Ordered<I>getOrder() 的返回值越小,优先级越高
  4. 若使用 @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 配置不允许在切入点表达式中用名称引入其他切入点
posted @   tree6x7  阅读(115)  评论(0编辑  收藏  举报
编辑推荐:
· .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 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示
主题色彩