Spring Ioc容器解读
一、概述
什么是容器?
容器是一种为某种特定组件的运行提供必要支持的一个软件环境。例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。类似Docker这样的软件也是一个容器,它提供了必要的Linux环境以便运行一个特定的Linux进程。
通常来说,使用容器运行组件,除了提供一个组件运行环境之外,容器还提供了许多底层服务。例如,Servlet容器底层实现了TCP连接,解析HTTP协议等非常复杂的服务,如果没有容器来提供这些服务,我们就无法编写像Servlet这样代码简单,功能强大的组件。早期的JavaEE服务器提供的EJB容器最重要的功能就是通过声明式事务服务,使得EJB组件的开发人员不必自己编写冗长的事务处理代码,所以极大地简化了事务处理。
Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
二、Ioc原理
Ioc介绍
IoC全称Inversion of Control,直译为控制反转
需求背景
-
实例化一个组件很难,有些时候需要读取配置,而不同的类又需要单独编写配置的代码,比较麻烦。
-
两个类实例化同一个组件,完全可以共用,但又不好决定谁负责创建和共享,不好处理。
-
组件需要销毁释放资源,但如果被多个组件共享,那怎么保证该不该被销毁??
-
依赖关系复杂。
传统的应用程序中,控制权在程序本身,程序的控制流程完全由开发者控制,在IoC模式下,控制权发生了反转,即从应用程序转移到了IoC容器,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件。为了能让组件在IoC容器中被“装配”出来,需要某种“注入”机制。
使用Bean配置
因为IoC容器要负责实例化所有的组件,因此,有必要告诉容器如何创建组件,以及各组件的依赖关系。一种最简单的配置是通过XML文件来实现
<beans>
<bean id="dataSource" class="HikariDataSource" />
<bean id="bookService" class="BookService">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userService" class="UserService">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
使用Annotation配置
@Component
public class MailService {
...
}
这个@Component注解就相当于定义了一个Bean,它有一个可选的名称,默认是mailService,即小写开头的类名。
然后,我们给UserService添加一个@Component注解和一个@Autowired注解:
@Component
public class UserService {
@Autowired
MailService mailService;
...
}
使用@Autowired就相当于把指定类型的Bean注入到指定的字段中。和XML配置相比,@Autowired大幅简化了注入,因为它不但可以写在set()方法上,还可以直接写在字段上,甚至可以写在构造方法中:
@Component
public class UserService {
MailService mailService;
public UserService(@Autowired MailService mailService) {
this.mailService = mailService;
}
...
}
使用Annotation配合自动扫描能大幅简化Spring的配置,我们只需要保证:
- 每个Bean被标注为@Component并正确使用@Autowired注入;
- 配置类被标注为@Configuration和@ComponentScan;
- 所有Bean均在指定包以及子包内。
定义了@Component才是Bean,没有这个注解是无法使用Autowired注解的。
定制Bean
对于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个变量进行验证。为了便于扩展,我们先定义验证接口:
@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);
}
}
}
因为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 {
...
}
如果不知道这个Bean是否存在,可以在Autowired加上required = false参数
注入配置
@Component
public class SmtpConfig {
@Value("${smtp.host}")
private String host;
@Value("${smtp.port:25}")
private int port;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}