2022-06-07:定制Bean
1、Scope
对于Spring容器来说,我们把一个Bean标记为@Component之后,它就会自动为我们创建一个单例(Singleton),即容器初始化时创建Bean,容器关闭前销毁Bean。在容器运行期间,我们调用getBean(Class)获取到的Bean总是同一个实例。
还有一种Bean,我们每次调用getBean(Class),容器都返回一个新的实例,这种Bean称为Prototype(原型),它的生命周期和Singleton不同。声明一个Prototype的Bean时,需要添加一个额外的@Scope注解:
@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype") public class MailSession { ... }
2、注入List
有时,我们会有一系列接口相同,不同实现类的Bean。例如,注册用户时,我们要对email、password和name这3个变量进行验证。为了便于扩展,我们先定义验证接口:
public interface Validator { void validate(String email, String password, String name); }
然后,分别用3个Validator对用户参数进行验证:
@Component public class EmailValidator implements Validator { public void validate(String email, String password, String name) { if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$")) { throw new IllegalArgumentException("invalid email: " + email); } } } @Component public class PasswordValidator implements Validator { public void validate(String email, String password, String name) { if (!password.matches("^.{6,20}$")) { throw new IllegalArgumentException("invalid password"); } } } @Component public class NameValidator implements Validator { public void validate(String email, String password, String name) { if (name == null || name.isBlank() || name.length() > 20) { throw new IllegalArgumentException("invalid name: " + name); } } }
最后,我们通过一个Validators作为入口进行验证:
@Component public class Validators { @Autowired List<Validator> validators; public void validate(String email, String password, String name) { for (var validator : this.validators) { validator.validate(email, password, name); } } }
注意到Validators被注入了一个List<Validator>,Spring会自动把所有类型为Validator的Bean装配为一个List注入进来,这样一来,我们每新增一个Validator类型,就自动被Spring装配到了Validators中了,非常方便。
因为Spring是通过扫描classpath获取到所有的Bean,而List是有序的,要指定List中Bean的顺序,可以加上@Order注解:
@Component @Order(1) public class EmailValidator implements Validator { ... } @Component @Order(2) public class PasswordValidator implements Validator { ... } @Component @Order(3) public class NameValidator implements Validator { ... }
3、可选注入
默认情况下,当我们标记了一个@Autowired后,Spring如果没有找到对应类型的Bean,它会抛出NoSuchBeanDefinitionException异常。
可以给@Autowired增加一个required = false的参数:
@Component public class MailService { @Autowired(required = false) ZoneId zoneId = ZoneId.systemDefault(); ... }
这个参数告诉Spring容器,如果找到一个类型为ZoneId的Bean,就注入,如果找不到,就忽略。
这种方式非常适合有定义就定义,没有就用默认值的情况。
4、创建第三方Bean
如果一个Bean不在我们的package管理之内,例如ZoneId,如何创建它?
答案是我们自己在@Configuration类中编写一个Java方法创建并返回它,注意给方法标记一个@Bean注解:
@Configuration @ComponentScan public class AppConfig { // 创建一个Bean: @Bean ZoneId createZoneId() { return ZoneId.of("Z"); } }
Spring对标记为@Bean的方法只调用一次,因此返回的Bean仍然是单例。
5、初始化和销毁
有些时候,一个Bean在注入必要的依赖后,需要进行初始化(监听消息等)。在容器关闭时,有时候还需要清理资源(关闭连接池等)。我们会通常定义一个init()方法进行初始化,定义一个shutdown()方法进行清理,然后,引入JSR-250定义的Annotation:
<dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency>
在Bean的初始化和清理方法上标记@PostConstruct和@PreDestroy:
@Component public class MailService { @Autowired(required = false) ZoneId zoneId = ZoneId.systemDefault(); @PostConstruct public void init() { System.out.println("Init mail service with zoneId = " + this.zoneId); } @PreDestroy public void shutdown() { System.out.println("Shutdown mail service"); } }
Spring容器会对上述Bean做如下初始化流程:
- 调用构造方法创建MailService实例;
- 根据@Autowired进行注入;
- 调用标记有@PostConstruct的init()方法进行初始化。
而销毁时,容器会先调用标记有@PreDestroy的shutdown()方法。
Spring只根据Annotation查找无参方法,对方法名不做要求。
6、使用别名
默认情况下,对一种类型的Bean,容器只创建一个实例。但有些时候,我们需要对一种类型的Bean创建多个实例。例如,同时连接多个数据库,就必须创建多个DataSource实例。
如果我们在@Configuration类中创建了多个同类型的Bean:
@Configuration @ComponentScan public class AppConfig { @Bean ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); } }
Spring会报错NoUniqueBeanDefinitionException,意思是出现了重复的Bean定义。
这个时候,需要给每个Bean添加不同的名字:
@Configuration @ComponentScan public class AppConfig { @Bean("z") ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); } }
可以用@Bean("name")指定别名,也可以用@Bean+@Qualifier("name")指定别名。
存入多个同类型的Bean时,注入ZoneId又会报错:
NoUniqueBeanDefinitionException: No qualifying bean of type
'java.time.ZoneId' available: expected single matching bean but found 2
意思是期望找到唯一的ZoneId类型Bean,但是找到了两个。因此注入时,要指定Bean的名称:
@Component public class MailService { @Autowired(required = false) @Qualifier("z") // 指定注入名称为"z"的ZoneId ZoneId zoneId = ZoneId.systemDefault(); ... }
还有一个方法是把其中的某个Bean指定为@Primary:
@Configuration @ComponentScan public class AppConfig { @Bean @Primary // 指定为主要Bean @Qualifier("z") ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean @Qualifier("utc8") ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); } }
这样,在注入时,如果没有指出Bean的名字,Spring会注入标记有@Primary的Bean。这种方式也很常用,例如,对于主从两个数据源,通常将主数据源定义为@Primary:
@Configuration @ComponentScan public class AppConfig { @Bean @Primary DataSource createMasterDataSource() { ... } @Bean @Qualifier("slave") DataSource createSlaveDataSource() { ... } }
其他Bean默认注入的就是主数据源,如果要注入从数据源,那么只需要指定名称即可。
7、使用FactoryBean
我们在设计模式的工厂方法中讲到,很多时候,可以通过工厂模式创建对象。Spring也提供了工厂模式,允许定义一个工厂,然后由工厂创建真正的Bean。
用工厂模式创建Bean需要实现FactoryBean接口。我们观察下边的代码:
@Component public class ZoneIdFactoryBean implements FactoryBean<ZoneId> { String zone = "Z"; @Override public ZoneId getObject() throws Exception { return ZoneId.of(zone); } @Override public Class<?> getObjectType() { return ZoneId.class; } }
当一个Bean实现了FactoryBean接口后,Spring会先实例化这个工厂,然后调用getObject()创建真正的Bean。getObjectType()可以指定创建的Bean的类型,因为指定类型与实际类型可能不一致,可以是接口或抽象类。
因此,如果定义了一个FactoryBean,要注意Spring创建的Bean实际上是这个FactoryBean的getObject()方法返回的Bean。为了和普通Bean区分,我们通常都以XxxFactoryBean命名。
小结
- Spring默认使用Singleton创建Bean,也可指定Scope为Prototype;
- 可以将相同类型的Bean注入List;
- 可用@Autowired(required=false)允许可选注入;
- 可用带@Bean标注的方法创建Bean;
- 可使用@PostConstruct和@PreDestroy对Bean进行初始化和清理;
- 相同类型的Bean只有一个可以指定为@Primary,其他必须用@Qualifier("beanName")指定别名;
- 注入时,可以通过别名@Qualifier("beanName")指定某个Bean;
- 可以定义FactoryBean来使用工厂模式创建Bean。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2021-06-07 QT:中文字符串与“常量中有字符串”报错
2020-06-07 Pycharm:设置自带控制台的python版本