1. 背景与动机
在 Spring 应用中,Bean 之间的依赖注入是家常便饭。但有时候,我们也会遇到一个实际需求:把 Spring 管理的 Bean 注入到一个“非托管对象”中。比如,你可能希望在 JPA 实体类里直接调用某个服务(Service)来生成 ID 或记录日志。
听起来有点“越界”?确实,这打破了传统 IoC 容器的管理边界。但 Spring 提供了方案:通过 @Configurable
注解配合 AspectJ 编织(weaving),就能实现这种“越界注入”。
⚠️ 注意:这不是常规操作,属于“高级玩法”,用不好容易踩坑。但了解它,能帮你理解 Spring 更底层的能力。
2. @Configurable
注解详解
这个注解的作用是:标记某个类,使其在创建时能被 Spring 自动装配(autowire)。哪怕这个对象是通过 new
关键字直接创建的,也能完成注入。
2.1 定义一个 Spring Bean
先准备一个简单的服务类,作为我们要注入的目标:
@Service
public class IdService {
private static int count;
int generateId() {
return ++count;
}
}
✅ 使用 @Service
注解,配合组件扫描(@ComponentScan
),Spring 就能自动注册这个 Bean。
接下来是一个配置类,开启组件扫描:
@ComponentScan
public class AspectJConfig {
}
2.2 基本使用 @Configurable
最简单的用法,直接在类上加注解即可:
@Configurable
public class PersonObject {
private int id;
private String name;
public PersonObject(String name) {
this.name = name;
}
// getter/setter 省略
}
此时,PersonObject
虽然是通过 new
创建的非托管对象,但 Spring 有机会在它初始化后进行配置。
2.3 注入 Spring Bean 到非托管对象
现在,我们尝试把 IdService
注入到 PersonObject
中:
@Configurable
public class PersonObject {
@Autowired
private IdService idService;
private int id;
private String name;
public PersonObject(String name) {
this.name = name;
}
void generateId() {
this.id = idService.generateId();
}
// getter/setter 省略
}
❌ 但注意:光加注解没用!@Autowired
不会自动生效,因为 Spring 容器根本不“知道”这个对象的存在。
✅ 真正起作用的是 AspectJ 的编译期或加载期编织(weaving),它会在对象创建时“插一脚”,完成依赖注入。核心是 Spring 提供的 AnnotationBeanConfigurerAspect
。
3. 启用 AspectJ 编织
要让 @Configurable
生效,必须引入 AspectJ 并配置编织过程。
3.1 Maven 插件配置
首先,在 pom.xml
中添加 AspectJ Maven 插件:
<plugin>
<groupId>dev.aspectj</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.13.1</version>
<configuration>
<complianceLevel>17</complianceLevel>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
关键点说明:
- ✅
complianceLevel
: 设置为 17,表示使用 JDK 17 编译。 - ✅
aspectLibraries
: 引入spring-aspects
,其中包含了AnnotationBeanConfigurerAspect
,这是实现@Configurable
的核心切面。 - ✅
executions
: 绑定compile
阶段,确保编译时完成编织。
同时,别忘了添加依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.5</version>
</dependency>
💡 提示:最新版本可在 Maven Central 查找。
3.2 启用 Spring 配置支持
最后一步,在配置类上加上 @EnableSpringConfigured
:
@ComponentScan
@EnableSpringConfigured
public class AspectJConfig {
}
✅ 这个注解会激活 AnnotationBeanConfigurerAspect
,让它监听所有 @Configurable
标记的类,并在对象创建时尝试注入依赖。
4. 测试验证
写个单元测试,验证注入是否成功:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AspectJConfig.class)
public class PersonUnitTest {
@Test
public void givenUnmanagedObjects_whenInjectingIdService_thenIdValueIsCorrectlySet() {
PersonObject personObject = new PersonObject("Baeldung");
personObject.generateId();
assertEquals(1, personObject.getId());
assertEquals("Baeldung", personObject.getName());
}
}
✅ 如果测试通过,说明即使 PersonObject
是通过 new
创建的,IdService
也成功注入并工作了。
5. 注入到 JPA 实体中的实践
实体类是最常见的“非托管对象”场景。Spring 并不管理 JPA 实体的生命周期,但我们有时仍想在实体中调用服务。
5.1 实体类定义
@Entity
@Configurable(preConstruction = true)
public class PersonEntity {
@Id
private int id;
private String name;
public PersonEntity() {
}
// 其他代码见下节
}
⚠️ 注意 preConstruction = true
:
这意味着依赖注入会在构造函数执行之前完成。否则,如果在构造函数中使用 idService
,会报 NullPointerException
,因为字段还没被注入。
5.2 注入服务到实体
@Entity
@Configurable(preConstruction = true)
public class PersonEntity {
@Autowired
@Transient
private IdService idService;
@Id
private int id;
private String name;
public PersonEntity() {
}
public PersonEntity(String name) {
id = idService.generateId(); // 构造时使用服务
this.name = name;
}
// getter/setter 省略
}
关键点:
- ✅
@Transient
: 告诉 JPA 不要持久化idService
字段。 - ✅
preConstruction = true
: 保证idService
在构造函数执行前已注入。
5.3 更新测试用例
@Test
public void givenUnmanagedObjects_whenInjectingIdService_thenIdValueIsCorrectlySet() {
PersonObject personObject = new PersonObject("Baeldung");
personObject.generateId();
assertEquals(1, personObject.getId());
PersonEntity personEntity = new PersonEntity("Baeldung");
assertEquals(2, personEntity.getId()); // 因为 count 已自增
assertEquals("Baeldung", personEntity.getName());
}
✅ 测试通过,说明实体类也能成功注入 Spring Bean。
6. 注意事项与设计权衡
虽然技术上可行,但这种做法有明显弊端,使用时务必谨慎:
- ❌ 破坏领域模型的纯洁性:实体类应只关注数据和业务逻辑,不应依赖外部服务。一旦注入 Bean,就和 Spring 耦合了,难以复用或测试。
- ❌ 隐藏的依赖关系:
new PersonEntity()
看似简单,实则背后依赖 Spring 容器,违反了“显式依赖”原则。 - ❌ 调试困难:注入发生在编织阶段,出问题时堆栈信息可能不直观。
- ❌ 性能开销:AspectJ 编织会增加编译和运行时复杂度。
✅ 建议使用场景:
- 工具类需要访问配置或日志服务。
- 临时方案或快速原型。
- 确实无法通过服务层传递依赖的极端情况。
❌ 不建议在核心领域模型中滥用。
7. 总结
本文介绍了如何通过 @Configurable
+ AspectJ 实现 在非托管对象中注入 Spring Bean,适用于实体类、DTO 或其他 new
出来的对象。
虽然技术上“简单粗暴”,但属于“双刃剑”——用得好是利器,用不好就是技术债。
💡 源码已托管至 GitHub:https://github.com/baeldung/spring-di-2
建议:优先考虑通过服务层传递依赖,实在绕不开再考虑此方案。毕竟,好的架构应该让依赖关系清晰可见,而不是藏在 new
之后。