强推!Java 大牛熬夜一周梳理的 Spring IOC 笔记,收藏一波
Hello,今天给各位童鞋们分享 Spring IOC,赶紧拿出小本子记下来吧!
1. IoC 原理
IoC 全称 Inversion of Control,直译为控制反转。
为什么要使用 IoC?
我们假定一个在线书店,通过 BookService 获取书籍:
public class BookService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
public Book getBook(long bookId) {
try (Connection conn = dataSource.getConnection()) {
...
return book;
}
}
}
为了从数据库查询书籍,BookService 持有一个 DataSource。为了实例
HikariDataSource,又不得不实例化一个 HikariConfig。现在,我们继续编
UserService 获取用户:
public class UserService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
public User getUser(long userId) {
try (Connection conn = dataSource.getConnection()) {
...
return user;
}
}
}
因为 UserService 也需要访问数据库,因此,我们不得不也实例化一个 HikariDataSource。
每一次调用方法, 都需要实例化一个 HikariDataSource,容易造成资源浪费。如果用某种方法实现了共享资源,那么怎么确保在所有功能完整的情况下,销毁以释放资源呢?
因此,核心问题是:
谁负责创建组件?
谁负责根据依赖关系组装组件?
销毁时,如何按依赖顺序正确销毁?
解决这一问题的核心方案就是 IoC。
传统的应用程序中,**控制权在程序本身,**程序的控制流程完全由开发者控制,即在程序内部进行实例化类。
而在 IoC 模式下,控制权发生了反转,即从应用程序转移到了 IoC 容器,所有组件不再由应用程序自己创建和配置,而是由 IoC 容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件。
为了能让组件在 IoC 容器中被“装配”出来,需要某种“注入”机制,例如,BookService 自己并不会创建 DataSource,而是等待外部通过 setDataSource()方法来注入一个 DataSource:
public class BookService {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
不直接 new 一个 DataSource,而是注入一个 DataSource,这个小小的改动虽然简单,却带来了一系列好处:
BookService 不再关心如何创建 DataSource,因此,不必编写读取数据库配置之类的代码;
DataSource 实例被注入到 BookService,同样也可以注入到 UserService,因此,共享一个组件非常简单;
测试 BookService 更容易,因为注入的是 DataSource,可以使用内存数据库,而不是真实的 MySQL 配置。
因此,IoC 又称为依赖注入(DI:Dependency Injection),它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由 IoC 容器负责管理组件的生命周期。
因为 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>
上述 XML 配置文件指示 IoC 容器创建 3 个 JavaBean 组件,并把 id 为 dataSource 的组件通过属性 dataSource(即调用 setDataSource()方法)注入到另外两个组件中。
在 Spring 的 IoC 容器中,我们把所有组件统称为 JavaBean,即配置一个组件就是配置一个 Bean。
依赖注入(DI:Dependency Injection)方式
从上面的代码我们可以得知,依赖注入可以通过 set()方法实现,但同时我们也可以通过构造方法来实现:
//set()方法
public class BookService {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
//构造器方法
public class BookService {
private DataSource dataSource;
public BookService(DataSource dataSource) {
this.dataSource = dataSource;
}
}
无侵入容器
在设计上,Spring 的 IoC 容器是一个高度可扩展的无侵入容器。所谓无侵入,是指应用程序的组件无需实现 Spring 的特定接口,或者说,组件根本不知道自己在 Spring 的容器中运行。这种无侵入的设计有以下好处:
应用程序组件既可以在 Spring 的 IoC 容器中运行,也可以自己编写代码自行组装配置;
测试的时候并不依赖 Spring 容器,可单独进行测试,大大提高了开发效率。
2. 装配 Bean 组件
我们来看一个具体的用户注册登录的例子。整个工程的结构如下:
我们先编写一个 MailService,用于在用户登录和注册成功后发送邮件通知:
public class MailService {
private ZoneId zoneId = ZoneId.systemDefault();
public void setZoneId(ZoneId zoneId) {
this.zoneId = zoneId;
}
public String getTime() {
return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
}
public void sendLoginMail(User user) {
System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));
}
public void sendRegistrationMail(User user) {
System.err.println(String.format("Welcome, %s!", user.getName()));
}
}
再编写一个 UserService,实现用户注册和登录:
注意到 UserService 通过 setMailService()注入了一个 MailService。然后,我们需要编写一个特定的 application.xml 配置文件,告诉 Spring 的 IoC 容器应该如何创建并组装 Bean:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.itranswarp.learnjava.service.UserService">
<property name="mailService" ref="mailService" />
</bean>
<bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
</beans>
注意观察上述配置文件,其中与 XML Schema 相关的部分格式是固定的,我们只关注两个<bean ...>的配置:
每个<bean ...>都有一个 id 标识,相当于 Bean 的唯一 ID;
在 userServiceBean 中,通过<property name="..." ref="..." />注入了另一个 Bean;
Bean 的顺序不重要,Spring 根据依赖关系会自动正确初始化。
最后一步,我们需要创建一个 Spring 的 IoC 容器实例,然后加载配置文件,让 Spring 容器为我们创建并装配好配置文件中指定的所有 Bean,这只需要一行代码:
ApplicationContextcontext=newClassPathXmlApplicationContext("application.xml");
接下来,我们就可以从 Spring 容器中“取出”装配好的 Bean 然后使用它:
// 获取 Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用:
User user = userService.login("bob@example.com", "password");
创建 Spring IoC 容器
ClassPathXmlApplicationContext(常用)
我们从创建 Spring 容器的代码:
ApplicationContextcontext=newClassPathXmlApplicationContext("application.xml");
可以看到,Spring 容器就是 ApplicationContext,它是一个接口,有很多实现类,这里我们选择 ClassPathXmlApplicationContext,表示它会自动从 classpath 中查找指定的 XML 配置文件。
从 ApplicationContext 中我们可以根据 Bean 的 ID 获取 Bean,但更多的时候我们根据 Bean 的类型获取 Bean 的引用:
UserService userService = context.getBean(UserService.class);
其中,userService 为实例化的一个类,得到的 userService 可以调用类中的方法。
BeanFactory
Spring 还提供另一种 IoC 容器叫 BeanFactory,使用方式和 ApplicationContext
类似:
BeanFactoryfactory=newXmlBeanFactory(newClassPathResource("application.xml"));
MailService mailService = factory.getBean(MailService.class);
BeanFactory 和 ApplicationContext 的区别在于,BeanFactory 的实现是按需创建,即第一次获取 Bean 时才创建这个 Bean,而 ApplicationContext 会一次性创建所有的 Bean。实际上,ApplicationContext 接口是从 BeanFactory 接口继承而来的,并且,ApplicationContext 提供了一些额外的功能,包括国际化支持、事件和通知机制等。通常情况下,我们总是使用 ApplicationContext,很少会考虑使用 BeanFactory。
3. 使用 Annotation 进行简化配置
我们可以使用 Annotation 配置,可以完全不需要 XML,让 Spring 自动扫描 Bean 并组装它们。
先删除 XML 配置文件,然后,给 UserService 和 MailService 添加几个注解。
首先,我们给 MailService 添加一个 @Component 注解:
@Component
public class MailService {
...
}
这个 @Component 注解就相当于定义了一个 Bean,它有一个可选的名称,默认是 mailService,即小写开头的类名。
然后,我们给 UserService 添加一个 @Component 注解和一个 @Autowired 注解:
@Component
public class UserService {
@Autowired
MailService mailService;
...
}
使用 @Autowired 就相当于把指定类型的 Bean 注入到指定的字段中。此外,还可以直接写在构造方法中:
最后,编写一个 AppConfig 类启动容器:
这里需要说明的是,
使用的实现类是 AnnotationConfigApplicationContext,所以必须传入一个标注了 @Configuration 的类名。
AppConfig 还标注了 @ComponentScan,它告诉容器,自动搜索当前类所在的包以及子包,把所有标注为 @Component 的 Bean 自动创建出来,并根据 @Autowired 进行装配。
使用 @ComponentScan 非常方便,但是,我们也要特别注意包的层次结构。通常来说,启动配置 AppConfig 位于自定义的顶层包,其他 Bean 按类别放入子包。
4. 定制 Bean 组件
Scope(@Scope(“prototype”))
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 个变量进行验证。为了便于扩展,我们先定义验证接口:
public interface Validator {
//定义方法
void validate(String email, String password, String name);
}
然后,分别使用 3 个 Validator 对用户参数进行验证:
最后,我们通过一个 Validators 作为入口进行验证:
注意到 Validators 被注入了一个 List<Validator>,Spring 会自动把所有类型为 Validator 的 Bean 装配为一个 List 注入进来,这样一来,我们每新增一个 Validator 类型,就自动被 Spring 装配到 Validators 中了,非常方便。
可选注入(无指定 Bean)
默认情况下,当我们标记了一个 @Autowired 后,Spring 如果没有找到对应类型的 Bean,它会抛出 NoSuchBeanDefinitionException 异常。
可以给 @Autowired 增加一个 required = false 的参数:
@Component
public class MailService {
@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
...
}
这个参数告诉 Spring 容器,如果找到一个类型为 ZoneId 的 Bean,就注入,如果找不到,就忽略。
这种方式非常适合有定义就使用定义,没有就使用默认值的情况。
创建第三方 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(prototype))
初始化和销毁
有些时候,一个 Bean 在注入必要的依赖后,需要进行初始化(监听消息等)。在容器关闭时,有时候还需要清理资源(关闭连接池等)。
在此之前,需要引入 JSR-250 定义的 Annotation:
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
在 Bean 的初始化和清理方法上标记 @PostConstruct 和 @PreDestroy:
Spring 容器会对上述 Bean 做如下初始化流程:
调用构造方法创建 MailService 实例;
根据 @Autowired 进行注入;
调用标记有 @PostConstruct 的 init()方法进行初始化。
而销毁时,容器会首先调用标记有 @PreDestroy 的 shutdown()方法。
Spring 只根据 Annotation 查找无参数方法,对方法名不作要求。
使用别名
当我们需要创建多个同类型的 Bean 时,我们就会用到别名:
可以用 @Bean("name")指定别名,也可以用 @Bean+@Qualifier("name")指定别名。
指定了别名后,注入时就需要指定 Bean 的名称,不然会报错:
@Component
public class MailService {
@Autowired(required = false)
@Qualifier("z") // 指定注入名称为"z"的 ZoneId
ZoneId zoneId = ZoneId.systemDefault();
...
}
或者指定默认 Bean,当注入时没有指定 Bean 的名字,则默认注入标记有 @Primary 的 Bean:
使用 FactoryBean(工厂模式)
用工厂模式创建 Bean 需要实现
FactoryBean 接口。我们观察下面的代码:
当一个 Bean 实现了 FactoryBean 接口后,Spring 会先实例化这个工厂,然后调用 getObject()创建真正的 Bean。getObjectType()可以指定创建的 Bean 的类型,因为指定类型不一定与实际类型一致,可以是接口或抽象类。
因此,如果定义了一个 FactoryBean,要注意 Spring 创建的 Bean 实际上是这个 FactoryBean 的 getObject()方法返回的 Bean。为了和普通 Bean 区分,我们通常都以 XxxFactoryBean 命名。
5. 使用 Resource 读取文件
在 Java 程序中,我们经常会读取配置文件、资源文件等。使用 Spring 容器时,我们也可以把“文件”注入进来,方便程序读取。
上图是工程的结构,我们需要读取 logo.txt 文件,通常情况下,我们需要写很多繁琐的代码,主要是为了定位文件,打开 InputStream。Spring 则提供了一个 org.springframework.core.io.Resource,可以直接注入:
也可以直接指定文件的路径,例如:
@Value("file:/path/to/logo.txt")
private Resource resource;
6. 注入配置(读取配置文件)
@PropertySource 注入配置
除了像 Resource 读取文件那样,Spring 容器提供了一个更简单的 @PropertySource
来自动读取配置文件。我们只需要在 @Configuration 配置类上再添加一个注解:
Spring 容器看到 @PropertySource("app.properties")注解后,自动读取这个配置文件,然后,我们使用 @Value 正常注入:
@Value("${app.zone:Z}")
String zoneId;
注意注入的字符串语法,它的格式如下:
"${app.zone}"表示读取 key 为 app.zone 的 value,如果 key 不存在,启动将报错;
"${app.zone:Z}"表示读取 key 为 app.zone 的 value,但如果 key 不存在,就使用默认值 Z
还可以把注入的注解写到方法参数中:
@BeanZoneId
createZoneId(@Value("${app.zone:Z}") String zoneId) {
return ZoneId.of(zoneId);
}
Bean 中标记,需要注入的地方再标记
另一种注入配置的方式是先通过一个简单的 JavaBean 持有所有的配置,例如,一个
SmtpConfig:
然后,在需要读取的地方,使用 #{smtpConfig.host}注入:
"#{smtpConfig.host}"的意思是,从名称为 smtpConfig 的 Bean 读取 host 属性,即调用 getHost()方法。
使用一个独立的 JavaBean 持有所有属性,然后在其他 Bean 中以 #{bean.property}注入的好处是,多个 Bean 都可以引用同一个 Bean 的某个属性。例如,如果 SmtpConfig 决定从数据库中读取相关配置项,那么 MailService 注入的 @Value("#{smtpConfig.host}")仍然可以不修改正常运行。
7. 使用条件装配
定义不同环境
Spring 为应用程序准备了 Profile 这一概念,用来表示不同的环境。例如,我们分别定义开发、测试和生产这 3 个环境:
native
test
production
创建某个 Bean 时,Spring 容器可以根据注解 @Profile 来决定是否创建。例如,以下配置:
如果当前的 Profile 设置为 test,则 Spring 容器会调用 createZoneIdForTest()创建 ZoneId,否则,调用 createZoneId()创建 ZoneId。注意到 @Profile("!test")表示非 test 环境。
在运行程序时,加上 JVM 参数-Dspring.profiles.active=test 就可以指定以 test 环境启动。
实际上,Spring 允许指定多个 Profile,例如:
-Dspring.profiles.active=test,master
可以表示 test 环境,并使用 master 分支代码。
要满足多个 Profile 条件,可以这样写:
@Bean
@Profile({ "test", "master" }) // 同时满足 test 和 master
ZoneId createZoneId() {
...
}
使用 Conditional(条件注解)决定是否创建 Bean
除了根据 @Profile 条件来决定是否创建某个 Bean 外,Spring 还可以根据
@Conditional 决定是否创建某个 Bean。
例如,我们对 SmtpMailService 添加如下注解:
@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
...
}
它的意思是,如果满足 OnSmtpEnvCondition 的条件,才会创建 SmtpMailService 这个 Bean。
public class OnSmtpEnvCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "true".equalsIgnoreCase(System.getenv("smtp"));
}
}
Spring 只提供了 @Conditional 注解,具体判断逻辑还需要我们自己实现。
好啦,今天的文章就到这里,希望能帮助到屏幕前迷茫的你们!
评论