定制Bean
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 {
...
}
注入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 {
...
}
可选注入
默认情况下,当我们标记了一个@Autowired
后,Spring如果没有找到对应类型的Bean,它会抛出NoSuchBeanDefinitionException
异常。
可以给@Autowired
增加一个required = false
的参数:
@Component
public class MailService {
@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
...
}
这个参数告诉Spring容器,如果找到一个类型为ZoneId
的Bean,就注入,如果找不到,就忽略。
这种方式非常适合有定义就使用定义,没有就使用默认值的情况。
创建第三方Bean
如果一个Bean不在我们自己的package管理之内,例如ZoneId
,如何创建它?
答案是我们自己在@Configuration
类中编写一个Java方法创建并返回它,注意给方法标记一个@Bean
注解:
@Configuration
@ComponentScan
public class AppConfig {
// 创建一个Bean:
@Bean
ZoneId createZoneId() {
return ZoneId.of("Z");
}
}
Spring对标记为@Bean
的方法只调用一次,因此返回的Bean仍然是单例。
初始化和销毁
有些时候,一个Bean在注入必要的依赖后,需要进行初始化(监听消息等)。在容器关闭时,有时候还需要清理资源(关闭连接池等)。我们通常会定义一个init()
方法进行初始化,定义一个shutdown()
方法进行清理,然后,引入JSR-250定义的Annotation:
- jakarta.annotation:jakarta.annotation-api:2.1.1
在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查找无参数方法,对方法名不作要求。
使用别名
默认情况下,对一种类型的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默认注入的就是主数据源。如果要注入从数据源,那么只需要指定名称即可。
使用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
命名。
由于可以用@Bean
方法创建第三方Bean,本质上@Bean
方法就是工厂方法,所以,FactoryBean
已经用得越来越少了。
练习
定制Bean。
小结
Spring默认使用Singleton创建Bean,也可指定Scope为Prototype;
可将相同类型的Bean注入List
或数组;
可用@Autowired(required=false)
允许可选注入;
可用带@Bean
标注的方法创建Bean;
可使用@PostConstruct
和@PreDestroy
对Bean进行初始化和清理;
相同类型的Bean只能有一个指定为@Primary
,其他必须用@Qualifier("beanName")
指定别名;
注入时,可通过别名@Qualifier("beanName")
指定某个Bean;
可以定义FactoryBean
来使用工厂模式创建Bean。