Skip to content

在 Spring 应用中使用 AspectJ (Using AspectJ with Spring Applications)

到目前为止,我们在本章中介绍的所有内容都是纯粹的 Spring AOP。在本节中,我们将探讨如果你的需求超出了 Spring AOP 的能力范围,如何使用 AspectJ 编译器或织入器 (Weaver) 来代替或补充 Spring AOP。

Spring 随附了一个小型 AspectJ 切面库,在发行版中作为 spring-aspects.jar 独立提供。你需要将其添加到类路径中才能使用其中的切面。接下来的小节将讨论该库的内容以及如何使用它,包括:

  • 使用 AspectJ 对领域对象进行依赖注入。
  • 通过 Spring IoC 配置 AspectJ 切面。
  • Spring 框架中的加载时织入 (Load-time Weaving, LTW)。

使用 AspectJ 对领域对象进行 Spring 依赖注入

Spring 容器实例化并配置应用上下文中定义的 Bean。此外,你也可以请求 Bean 工厂配置一个已经存在的对象。spring-aspects.jar 包含一个注解驱动的切面,利用这一能力允许对任何对象进行依赖注入。

该支持旨在用于那些不受任何容器控制而创建的对象。领域对象 (Domain Objects) 通常属于此类,因为它们通常是使用 new 运算符编程式创建的,或者是 ORM 工具作为数据库查询结果创建的。

@Configurable 注解将一个类标记为符合 Spring 驱动配置的条件。在最简单的情况下,你可以仅将其用作标记注解:

java
package com.xyz.domain;

import org.springframework.beans.factory.annotation.Configurable;

@Configurable
public class Account {
	// ...
}
kotlin
package com.xyz.domain

import org.springframework.beans.factory.annotation.Configurable

@Configurable
class Account {
	// ...
}

当这样作为标记接口使用时,Spring 使用与完全限定类型名称 (com.xyz.domain.Account) 相同的 Bean 定义(通常是 prototype 作用域)来配置该类型的新实例。在 XML 中,可以通过省略 id 属性来方便地声明此定义:

xml
<bean class="com.xyz.domain.Account" scope="prototype">
	<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果你想显式指定要使用的 prototype Bean 定义的名称,可以直接在注解中设置:

java
@Configurable("account")
public class Account {
	// ...
}
kotlin
@Configurable("account")
class Account {
	// ...
}

Spring 现在将查找名为 account 的 Bean 定义,并将其用作配置新 Account 实例的蓝图。

你还可以使用自动装配 (Autowiring) 来避免指定专用的 Bean 定义。通过 autowire 属性可以指定 Autowire.BY_TYPEAutowire.BY_NAME。然而,更推荐在字段或方法级别使用 @Autowired@Inject 进行显式的注解驱动依赖注入。

最后,你可以通过 dependencyCheck=true 启用依赖检查,确保所有非原生、非集合属性在配置后都已被正确设置。

TIP

该注解本身不做任何事情。必须依赖 spring-aspects.jar 中的 AnnotationBeanConfigurerAspect。简而言之,该切面的逻辑是:“在带 @Configurable 注解的类型的新对象初始化返回后,根据注解的属性使用 Spring 来配置该对象。” 这里的“初始化”既包括 new 出来的对象,也包括正在反序列化的对象。

构造函数中的依赖使用

默认情况下,依赖注入发生在对象构造之后。这意味着依赖项在构造函数体中是不可用的。如果你希望在构造函数执行前注入依赖,以便在构造函数内部使用它们,需要按如下方式定义:

java
@Configurable(preConstruction = true)

单元测试 @Configurable 对象

如果不使用 AspectJ 织入,该注解在单元测试期间不会产生任何影响。你可以正常手动设置 mock 或 stub 引用。如果使用了 AspectJ 织入,虽然可以正常测试,但每次构造对象时你都会看到一条警告消息,提示它未被 Spring 配置。

多个应用上下文的处理

AnnotationBeanConfigurerAspect 是一个 AspectJ 单例切面。单例切面的范围与 static 成员相同:每个 ClassLoader 只有一个切面实例。这意味着如果你在同一个加载器层次结构中有多个上下文,你通常应该在共享的父上下文中定义 @EnableSpringConfigured,以确保切面持有正确的、用于领域对象注入的服务引用。

其他用于 AspectJ 的 Spring 切面

除了 @Configurable 切面外,spring-aspects.jar 还包含一个 AspectJ 切面,用于为带 @Transactional 注解的类型和方法驱动 Spring 的事务管理。这主要面向想要在 Spring 容器之外使用事务支持的用户。

该切面是 AnnotationTransactionAspect。使用时,必须注解在实现类(或其方法)上,而不是接口上。AspectJ 遵循 Java 的规则:接口上的注解是不被继承的。

使用 Spring IoC 配置 AspectJ 切面

当你在 Spring 应用中使用 AspectJ 切面时,自然希望能够通过 Spring 来配置这些切面。AspectJ 运行时负责创建切面,系统通过 Spring 配置这些切面的方式取决于切面的实例化模型(per-xxx 子句)。

大多数 AspectJ 切面是单例的。配置这些切面很简单:创建一个 Bean 定义并包含 factory-method="aspectOf" 属性。这确保了 Spring 是向 AspectJ 请求切面实例,而不是尝试自己创建一个。

xml
<bean id="profiler" class="com.xyz.profiler.Profiler" factory-method="aspectOf">
	<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

Spring 框架中的加载时织入 (Load-time Weaving)

加载时织入 (LTW) 是指在 AspectJ 切面被加载到 Java 虚拟机 (JVM) 时,将其织入到应用类文件中的过程。

Spring LTW 的核心价值在于提供更细粒度的控制。原生的 AspectJ LTW 通常是在 JVM 启动时通过 -javaagent 参数开启的全局设置。而 Spring 允许在每个 ClassLoader 的基础上开启 LTW,这在单虚拟机多应用的服务器环境中(如 Tomcat)非常有用。

第一个示例

假设你正在诊断性能问题,并希望开启一个简单的性能分析切面。

  1. 编写切面:使用 @AspectJ 风格定义一个 ProfilingAspect
  2. 配置 META-INF/aop.xml:告诉 AspectJ 需要织入哪些类。
  3. Spring 配置:开启 LTW。
xml
<!-- 开启加载时织入 -->
<context:load-time-weaver/>
  1. 运行程序:启动 JVM 时指定 Spring 的代理探针: java -javaagent:path/to/spring-instrument.jar com.xyz.Main

这样,即使是使用 new 关键字直接创建的对象,也会被切面拦截并进行性能审计。

Spring 配置详情

Spring LTW 支持的关键组件是 LoadTimeWeaver 接口。Spring 会根据你的运行时环境自动检测并使用相应的实现:

  • Tomcat: TomcatLoadTimeWeaver
  • JBoss/WildFly: JBossLoadTimeWeaver
  • 其他环境: 降级使用 InstrumentationLoadTimeWeaver (需配合 spring-instrument.jar)。

可以通过 Java 配置开启:

java
@Configuration
@EnableLoadTimeWeaving
public class ApplicationConfiguration {
}

补充教学

1. Spring AOP 与 AspectJ 的本质区别

这是 AOP 领域最重要的认知分水岭:

维度Spring AOPAspectJ
织入原理代理 (Proxy):通过运行时创建代理类包装目标对象。织入 (Weaving):直接修改字节码(编译时或加载时)。
能力上限仅支持 Spring Bean、方法执行。支持构造函数、字段访问、静态初始化、私有方法等。
自调用问题存在this.foo() 会失效)。不存在(因为代码逻辑已被直接修改)。
对象范围必须由 Spring 容器管理的 Bean。任何对象,即便你是 new 出来的。

2. 什么是 @Configurable?什么时候该用它?

在典型的 Web 应用中,Controller 调用 Service,Service 调用 Repository。这些都是单例且由 Spring 管理的 Bean,使用普通的 Spring AOP 即可。

但考虑一个富领域模型 (Rich Domain Model): 一个 User 实体类,它不是 Spring Bean。如果你希望 user.changePassword() 方法能够直接调用 passwordEncoder.encode()(这需要注入一个加密服务),在没有 AspectJ 的情况下,你必须手动传值或者在代码里硬编码。 通过 @Configurable 和 AspectJ LTW,生成的 User 对象在构造那一刻就会被“注入”所需的服务,让对象更加自洽。

3. Load-time Weaving (LTW) 的实战建议

  • 避坑指南:LTW 依赖于 Java Agent。在容器化/微服务时代,确保你的 Dockerfile 启动脚本中包含了 -javaagent:/path/to/spring-instrument.jar
  • 范围控制:务必在 aop.xml 中使用 <include within="com.yourcompany..*"/>。否则,AspectJ 可能会尝试织入所有的类(包括 JDK 类库),严重延长项目启动时间。
  • Spring Boot 用户:Spring Boot 默认并不启用 LTW。如果你需要它,除了引入 spring-aspects 依赖,还需要在启动类加上 @EnableLoadTimeWeaving

4. 总结:如何选择?

  • 90% 的场景:使用 Spring AOP(基于注解,简单、够用、无副作用)。
  • 性能极度敏感:或者需要拦截非 Spring Bean 的方法调用,选择 AspectJ 静态织入。
  • 需要在运行时灵活开启 AOP 且不改动构建流程:选择 AspectJ LTW。

Based on Spring Framework.