@Bean 详解
定义与作用
@Bean 是 Spring Java Config 配置方式的核心注解,用于在标注了 @Configuration 的类中显式声明一个 Bean 的定义。与组件扫描(@ComponentScan)自动发现类路径下的 @Component、@Service 等注解不同,@Bean 赋予开发者对 Bean 创建过程的完全控制权:你可以决定如何实例化对象、如何设置其属性、如何在创建前后插入自定义逻辑。
在飞翔科技的学生管理系统中,架构师白歌经常遇到这样的场景:某些第三方库提供的类(如数据库连接池、消息队列客户端)无法修改源码,自然也无法在其类上加 @Component。此时,@Bean 就是唯一能将这类对象纳入 Spring 容器管理的途径。
适用位置与常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
name / value | String[] | 指定 Bean 的名称,默认取方法名 |
initMethod | String | 指定初始化方法名,等价于 @PostConstruct |
destroyMethod | String | 指定销毁方法名,等价于 @PreDestroy,默认值为 (inferred) 会自动探测 close / shutdown 方法 |
适用位置:只能标注在方法上,且该方法必须定义在 @Configuration 类中(或在 @Component 类中,但行为有差异,见易错场景)。
核心原理
当 Spring 容器加载一个 @Configuration 类时,会通过 CGLIB 生成该类的代理子类。代理子类会拦截所有 @Bean 方法的调用,确保同一个 @Bean 方法在多次被依赖引用时,返回的是容器中的同一个实例(对于 singleton 作用域)。
关键机制:@Configuration 类中的 @Bean 方法之间的互相调用(如 jdbcTemplate() 调用 dataSource())也会被代理拦截,从而保证依赖的 Bean 同样是经过容器管理的单例,而非普通 Java 方法调用产生的新对象。
完整示例
场景简述
飞翔科技的学生管理系统需要连接 MySQL 数据库。后端开发小崔负责配置数据源和 JdbcTemplate。由于 HikariDataSource 和 JdbcTemplate 都是第三方类,无法直接加 @Component,小崔决定用 @Bean 显式装配。
操作前:无 Spring 管理的纯手动创建
// 没有 Spring 容器,每次需要数据源的地方都手动 new
public class StudentDao {
private javax.sql.DataSource dataSource;
public StudentDao() {
// 硬编码配置,无法切换环境
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db");
config.setUsername("root");
config.setPassword("secret");
this.dataSource = new HikariDataSource(config);
}
public List<Student> findAll() {
// 每次都要手动处理连接关闭
try (Connection conn = dataSource.getConnection()) {
// ... 查询逻辑
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
痛点分析:
- 配置与代码耦合,测试环境、生产环境切换困难
- 连接池无法复用,每个 DAO 都创建一个,资源浪费
- 没有生命周期管理,应用关闭时连接池无法优雅释放
使用该注解的完整代码
package com.feixiang.student.config;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "close")
@Profile("dev")
public DataSource devDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/student_dev");
config.setUsername("dev_user");
config.setPassword("dev_pass");
config.setMaximumPoolSize(5);
return new HikariDataSource(config);
}
@Bean(destroyMethod = "close")
@Profile("prod")
public DataSource prodDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://db.learnto.cn:3306/student_prod");
config.setUsername("prod_user");
config.setPassword("prod_pass");
config.setMaximumPoolSize(20);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
package com.feixiang.student.dao;
import com.feixiang.student.entity.Student;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class StudentDao {
private final JdbcTemplate jdbcTemplate;
// 构造器注入:Spring 自动将 @Bean 定义的 JdbcTemplate 注入进来
public StudentDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<Student> findAll() {
String sql = "SELECT id, name, age, major FROM student";
return jdbcTemplate.query(sql, (rs, rowNum) -> new Student(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("age"),
rs.getString("major")
));
}
}
package com.feixiang.student;
import com.feixiang.student.config.DataSourceConfig;
import com.feixiang.student.dao.StudentDao;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class StudentApplication {
public static void main(String[] args) {
// 激活 dev 环境
System.setProperty("spring.profiles.active", "dev");
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(DataSourceConfig.class);
StudentDao studentDao = ctx.getBean(StudentDao.class);
System.out.println("查询到的学生数量:" + studentDao.findAll().size());
ctx.close(); // 容器关闭时,HikariDataSource 的 close() 方法会被自动调用
}
}
操作后运行结果及分析
查询到的学生数量:3
变化分析:
- 配置与代码解耦:数据库连接信息集中在
DataSourceConfig中,业务代码StudentDao不再关心数据源如何创建 - 单例复用:
devDataSource()和jdbcTemplate()返回的实例被缓存于 Spring 容器的单例池中,StudentDao和其他 DAO 共享同一个连接池和 JdbcTemplate - 生命周期托管:
@Bean(destroyMethod = "close")确保容器关闭时调用HikariDataSource.close(),释放所有数据库连接 - 环境切换:通过
@Profile配合spring.profiles.active,开发环境连本地库,生产环境连线上库,无需修改任何 Java 代码
易错场景与面试考点
易错场景一:在 @Component 类中使用 @Bean
@Component
public class WrongConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate() {
// 这里的 dataSource() 是普通方法调用,会创建一个新的 DataSource!
return new JdbcTemplate(dataSource());
}
}
后果:jdbcTemplate() 内部调用的 dataSource() 不会被 Spring 代理拦截,导致创建了两个不同的 DataSource 实例:一个被 Spring 容器管理,一个被 JdbcTemplate 私有持有。连接池配置失效,资源泄漏。
正确做法:将 @Component 改为 @Configuration,或确保 @Bean 方法之间的依赖通过方法参数传入(Spring 会自动注入容器中的 Bean):
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource); // 容器自动注入已存在的单例
}
面试考点
Q:@Bean 的方法名和 Bean 名称有什么关系?
默认情况下,Bean 的名称就是方法名。可以通过
@Bean("customName")或@Bean(name = "customName")自定义。如果方法名重载,Spring 会报错,因为 Bean 名称必须唯一。
Q:@Bean 和 @Component 有什么区别?
@Component及其派生注解(@Service、@Repository等)用于标记类,由组件扫描自动发现并注册为 Bean,适用于自己编写的类。@Bean用于标记方法,在配置类中显式定义 Bean 的创建逻辑,适用于第三方库的类或需要复杂初始化逻辑的场景。
Q:@Configuration 和 @Component 中定义 @Bean 的最大区别是什么?
@Configuration类会被 CGLIB 代理,确保@Bean方法之间的内部调用被拦截,返回容器中的单例;而@Component类中的@Bean方法只是普通方法调用,每次调用都会执行方法体,可能创建新实例。