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
对用户参数进行验证:
@Componentpublic 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); } }}@Componentpublic class PasswordValidator implements Validator { public void validate(String email, String password, String name) { if (!password.matches("^.{6,20}$")) { throw new IllegalArgumentException("invalid password"); } }}@Componentpublic 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
作为入口进行验证:
@Componentpublic 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
的参数:
@Componentpublic class MailService { @Autowired(required = false) ZoneId zoneId = ZoneId.systemDefault(); ...}
这个参数通知Spring容器,如果找到一个类型为ZoneId
的Bean,就注入,如果找不到,就疏忽。
这种形式非常适合有定义就应用定义,没有就应用默认值的状况。
创立第三方Bean
如果一个Bean不在咱们本人的package治理之内,例如ZoneId
,如何创立它?
答案是咱们本人在@Configuration
类中编写一个Java办法创立并返回它,留神给办法标记一个@Bean
注解:
@Configuration@ComponentScanpublic 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
:
@Componentpublic 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@ComponentScanpublic class AppConfig { @Bean ZoneId createZoneOfZ() { return ZoneId.of("Z"); } @Bean ZoneId createZoneOfUTC8() { return ZoneId.of("UTC+08:00"); }}
Spring会报NoUniqueBeanDefinitionException
异样,意思是呈现了反复的Bean定义。
这个时候,须要给每个Bean增加不同的名字:
@Configuration@ComponentScanpublic 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的名称:
@Componentpublic class MailService { @Autowired(required = false) @Qualifier("z") // 指定注入名称为"z"的ZoneId ZoneId zoneId = ZoneId.systemDefault(); ...}
还有一种办法是把其中某个Bean指定为@Primary
:
@Configuration@ComponentScanpublic 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@ComponentScanpublic class AppConfig { @Bean @Primary DataSource createMasterDataSource() { ... } @Bean @Qualifier("slave") DataSource createSlaveDataSource() { ... }}
其余Bean默认注入的就是主数据源。如果要注入从数据源,那么只须要指定名称即可。
应用FactoryBean
咱们在设计模式的工厂办法中讲到,很多时候,能够通过工厂模式创建对象。Spring也提供了工厂模式,容许定义一个工厂,而后由工厂创立真正的Bean。
用工厂模式创立Bean须要实现FactoryBean
接口。咱们察看上面的代码:
@Componentpublic 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
曾经用得越来越少了。