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 之后。


原始标题:Injecting Spring Beans into Unmanaged Objects