在Spring框架的进阶学习中,理解并熟练运用IoC(控制反转)与DI(依赖注入)是区分普通使用者和深度掌握者的关键。如果说类注解是Spring容器的入门钥匙,那么方法注解@Bean和灵活的依赖注入策略,则是构建复杂、健壮企业级应用的基石。本文将深入探讨@Bean注解的妙用、Spring的组件扫描机制,并详细对比分析属性注入、构造方法注入和Setter注入这三种核心DI模式的优劣与适用场景,助你从“使用容器”真正迈向“设计容器”。
一、@Bean注解:灵活定义Spring Bean的利器
类注解(如@Component, @Service)适用于标注我们自己编写的类。但在实际开发中,我们常常需要将第三方库的类(源码不可修改)或同一个类的多个不同配置实例纳入Spring容器管理。这时,方法注解@Bean便闪亮登场。它允许我们在配置类中通过定义一个方法,来显式地创建和配置一个Bean对象,并将其注册到IoC容器中。
1.1 配合类注解使用:配置类的身份标识
@Bean注解本身并不能独立工作,它必须声明在一个被Spring管理的类中,即这个类本身需要被Spring扫描到。通常,我们会将@Bean方法写在带有@Configuration注解的配置类里。@Configuration是@Component的元注解,它标志着这个类是Bean定义的来源。
尝试在一个普通类中使用@Bean:
package com.yang.test1_26_1.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private int age;
private String name;
// 表示这个方法返回的实例将被Spring管理
@Bean
public UserInfo userInfo() {
return new UserInfo(18, "Jack");
}
}
然后尝试从容器中获取这个Bean:
package com.yang.test1_26_1;
import com.yang.test1_26_1.model.UserInfo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1261Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1261Application.class, args);
UserInfo bean = context.getBean(UserInfo.class);
System.out.println(bean);
}
}
运行程序,你会立刻遭遇NoSuchBeanDefinitionException异常,因为Spring根本不知道UserConfig这个类的存在。

解决方案很简单:为配置类添加五大类注解之一(如@Configuration),使其被Spring扫描并管理。

这就像在Java中调用一个方法,必须先有这个类的实例;在Go语言中,要使用一个包的功能,必须先import它。Spring的容器管理遵循着类似的逻辑前提。
[AFFILIATE_SLOT_1]1.2 定义多个对象与Bean命名
@Bean最强大的能力之一,就是可以为同一个类创建多个具有不同配置的Bean实例。这在多数据源、多缓存客户端等场景下极为常见。
例如,根据不同的配置创建两个UserInfo对象:
package com.yang.test1_26_2;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Controller
public class UserInfo {
private int age;
private String name;
@Bean
public UserInfo userInfo1() {
return new UserInfo(20, "Crane");
}
@Bean
public UserInfo userInfo2() {
return new UserInfo(18, "Larry");
}
}
尝试注入时:
package com.yang.test1_26_2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1262Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1262Application.class, args);
UserInfo bean1 = context.getBean(UserInfo.class);
System.out.println(bean1);
}
}
此时运行会报错,因为Spring发现有两个类型为UserInfo的Bean,不知道应该注入哪一个。

错误信息清晰地指出:找到了两个Bean,名字分别是userInfo1和userInfo2。默认情况下,@Bean方法的方法名就是Bean在容器中的名称。我们可以通过指定名称来注入:
package com.yang.test1_26_2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1262Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1262Application.class, args);
UserInfo bean1 = (UserInfo) context.getBean("userInfo1");
System.out.println(bean1);
UserInfo bean2 = (UserInfo) context.getBean("userInfo2");
System.out.println(bean2);
}
}
成功运行结果:

1.3 Bean的重命名艺术
我们可以通过@Component("自定义名称")或@Bean(name = {"别名1", "别名2"})来为Bean起一个或多个名字。这对于统一接口的不同实现、或者提供向后兼容的别名非常有用。
类注解重命名示例:
package com.yang.test1_27_1;
import org.springframework.stereotype.Controller;
@Controller("userController1")
public class UserController {
public void hello() {
System.out.println("hello Controller...");
}
}
package com.yang.test1_27_1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1271Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1271Application.class, args);
UserController bean = (UserController) context.getBean("userController1");
bean.hello();
}
}
方法注解重命名示例:
package com.yang.test1_27_1;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Controller
public class UserInfo {
private int age;
private String name;
@Bean("userInfoOne")
public UserInfo userInfo1() {
return new UserInfo(20, "Crane");
}
@Bean("userInfoTwo")
public UserInfo userInfoT2() {
return new UserInfo(18, "Larry");
}
}
package com.yang.test1_27_1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1271Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1271Application.class, args);
UserInfo bean1 = (UserInfo) context.getBean("userInfoOne");
System.out.println(bean1);
UserInfo bean2 = (UserInfo) context.getBean("userInfoTwo");
System.out.println(bean2);
}
}
关键规则:@Bean注解的name/value属性是一个字符串数组。一旦显式指定了它们,方法名就不再是Bean的名字。如果数组只有一个元素,花括号{}可以省略。这种灵活的命名机制,与TypeScript中的类型别名(type alias)或C++中的命名空间(namespace)有异曲同工之妙,都是为了更好地组织和管理代码实体。
package com.yang.test1_28_1;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Controller;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Controller
public class UserInfo {
private int age;
private String name;
@Bean({"userInfoOne", "u1"})
public UserInfo userInfo1() {
return new UserInfo(20, "Crane");
}
@Bean({"userInfoTwo", "u2"})
public UserInfo userInfoT2() {
return new UserInfo(18, "Larry");
}
}
package com.yang.test1_28_1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1281Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1281Application.class, args);
UserInfo bean1 = (UserInfo) context.getBean("u1");
System.out.println(bean1);
UserInfo bean2 = (UserInfo) context.getBean("u1");
System.out.println(bean2);
}
}
二、揭秘Spring的扫描路径:Bean生效的边界
SpringBoot遵循“约定大于配置”的原则。一个类即使被正确地标注了注解,也未必能被Spring容器管理。其生效的决定性前提是:该类所在的包必须在Spring的扫描路径之下。
扫描路径可以通过@ComponentScan注解进行配置:
@ComponentScan({"com.example.demo.component", "com.example.demo.service"}) // 扫描多个包
@SpringBootApplication
public class SpringIocDemoApplication { ... }
但在日常SpringBoot开发中,我们很少手动添加@ComponentScan。这是因为核心注解@SpringBootApplication已经包含了它,其默认扫描的起始路径就是主启动类所在的包及其所有子包。理解这一点,对于解决“找不到Bean”的诡异问题至关重要。这类似于JavaScript项目中的模块解析路径,或者Go语言中的GOPATH/module路径,定义了框架寻找资源的范围。
三、深度解析:Spring依赖注入的三种模式
依赖注入(DI)是IoC思想的具体实现,指容器在创建Bean时,自动将其所依赖的其他Bean(资源)传递给它。Spring提供了三种主流的注入方式,各有千秋。
专栏:JavaEE 进阶跃迁营
个人主页:手握风云
3.1 属性注入:简洁但需谨慎
属性注入(Field Injection)通过在类的成员变量上直接添加@Autowired注解来实现。这是最简洁、最常用的方式。
示例:将UserService注入到UserRepository中。
package com.yang.test1_28_2.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void hello() {
System.out.println("hello, Service...");
}
}
package com.yang.test1_28_2.repo;
import com.yang.test1_28_2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
@Autowired
private UserService userService;
public void hello() {
userService.hello();
System.out.println("hello, Repository...");
}
}
package com.yang.test1_28_2;
import com.yang.test1_28_2.repo.UserRepository;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1282Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1282Application.class, args);
UserRepository bean = context.getBean(UserRepository.class);
bean.hello();
}
}
从上下文中获取并测试:

如果移除@Autowired注解,依赖将不会被注入,导致空指针异常。

✅ 优点:
- 极其简洁:代码侵入性最小,只需一个注解。
- 强依赖容器:脱离Spring环境(如直接
new)会导致NPE。 - 无法注入final字段:因为final字段必须在构造时初始化,而属性注入发生在构造之后。
- 隐藏依赖,测试不便:依赖关系隐藏在注解背后,不直观,且单元测试时需要额外模拟容器。
3.2 构造方法注入:Spring官方推荐的方式
构造方法注入(Constructor Injection)通过类的构造方法来接收依赖。这是目前Spring团队最推荐的方式。
基本用法:
package com.yang.test1_28_2.controller;
import com.yang.test1_28_2.service.UserService;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
public void hello() {
userService.hello();
System.out.println("hello, Controller...");
}
}
package com.yang.test1_28_2;
import com.yang.test1_28_2.controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1282Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1282Application.class, args);
UserController bean = context.getBean(UserController.class);
bean.hello();
}
}
重要特性:如果一个类只有一个构造方法,那么@Autowired注解可以省略,Spring会自动选择它进行注入。这简化了代码。
✅ 核心优点:
- 支持final字段:完美解决了属性注入的短板,可以注入
final修饰的依赖,保证了依赖的不可变性(Immutability),这是函数式编程和现代Java实践所倡导的。
@Controller
public class UserController2 {
private final UserService userService; // final 修饰的依赖
// 构造方法注入 final 属性
public UserController2(UserService userService) {
this.userService = userService;
}
}
- 依赖不可变:通过构造方法设置后,没有setter就无法更改,对象状态更稳定。
- 完全初始化:对象在构造完成时,所有依赖就已就绪,避免了部分初始化状态,杜绝了NPE风险。
- 框架无关性:纯粹基于Java语法,不依赖Spring特定注解,迁移到其他IoC框架或进行普通实例化都更容易。
// 依赖较多时,构造方法参数冗长
public UserController2(UserService userService, UserRepository userRepository, UserConfig userConfig) {
this.userService = userService;
this.userRepository = userRepository;
this.userConfig = userConfig;
}
[AFFILIATE_SLOT_2]
3.3 Setter注入:灵活性的双刃剑
Setter注入通过在setter方法上添加@Autowired注解来实现。
示例:
package com.yang.test1_28_2.controller;
import com.yang.test1_28_2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void hello() {
userService.hello();
System.out.println("hello, Controller...");
}
}
package com.yang.test1_28_2;
import com.yang.test1_28_2.controller.UserController;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Test1282Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Test1282Application.class, args);
UserController bean = context.getBean(UserController.class);
bean.hello();
}
}
✅ 优点:
- 高度灵活:允许在对象创建后重新配置或更换依赖。适用于需要动态切换策略或数据源的场景。
// 对象创建后,可重新注入新的 UserService 实例
UserController3 controller3 = context.getBean(UserController3.class);
controller3.setUserService(new UserService() { // 新的实现
@Override
public void sayHi() {
System.out.println("Hi, 新的 UserService 实现");
}
});
controller3.sayHi(); // 输出:Hi, 新的 UserService 实现
- 可读性明确:每个setter方法明确对应一个依赖的注入。
- 无法注入final字段:同属性注入。
- 存在状态被篡改的风险:setter方法可能被意外调用,破坏对象的一致性。
- 可能导致部分初始化状态:如果在构造方法中使用了通过setter注入的依赖,会引发NPE。
- 强依赖Spring:与属性注入类似,脱离了
@Autowired注解,该模式就失效了。
总结与最佳实践建议
通过本文的探讨,我们深入掌握了Spring IoC容器中@Bean注解的强大功能与三种依赖注入模式的精髓。@Bean让我们能够灵活地管理任何对象的生命周期,而三种DI模式则为我们提供了应对不同场景的武器。
最佳实践总结:
- 首选构造方法注入:尤其对于强制性的、不变的依赖。它能保证对象的不可变性和完全初始化,是现代Java应用(包括Spring Boot 2.x+官方文档推荐)的首选。
- 谨慎使用属性注入:虽然方便,但应仅限于非强制性的、辅助性的依赖(如Logger),或在一些特定框架(如JPA Entity)中不得已而为之。避免在业务核心逻辑中大量使用。
- 按需使用Setter注入:当依赖确实是可选的,或需要在运行时动态变更时,Setter注入是合适的工具。
- 善用@Bean管理第三方Bean:将外部库的组件、复杂对象构造逻辑清晰地封装在@Configuration类的@Bean方法中,使配置集中且明确。
浙公网安备 33010602011771号