Spring Framework主要包括几个模块:

  • 支持IoC和AOP的容器;
  • 支持JDBC和ORM的数据访问模块;
  • 支持声明式事务的模块;
  • 支持基于Servlet的MVC开发;
  • 支持基于Reactive的Web开发;
  • 以及集成JMS、JavaMail、JMX、缓存等其他模块。

IoC全称Inversion of Control,译为控制反转。

why

如果一个系统有大量的组件,采用new创建实例的方式来持有,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。

而IoC就可以用于解决这一系列核心问题:

  • 负责创建组件
  • 负责根据依赖关系组装组件
  • 销毁时,按依赖顺序正确销毁

传统的应用程序中,控制权在程序本身,程序的控制流程完全由开发者控制。这种模式的缺点是,一个组件如果要使用另一个组件,必须先知道如何正确地创建它。

在IoC模式下,控制权发生了反转,即从应用程序转移到了IoC容器,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件。为了能让组件在IoC容器中被“装配”出来,需要某种“注入”机制,即等待外部的set(...)方法来传入组件

通过注入,我们获得了以下几点便利:

  • 无需关心如何创建,只需将引用指向注入的组件即可
  • 共享组件容易
  • 测试方便,我们可以传入容易获取的组件来测试正确性

因此,IoC又称为依赖注入(DI:Dependency Injection)

我们需要告诉容器如何创建组件,以及各组件的依赖关系。最简单的是配置XML文件,Spring容器通过读取XML文件后使用反射完成:

1
2
3
4
5
6
7
8
9
<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>

在Spring的IoC容器中,我们把所有组件统称为JavaBean,即配置一个组件就是配置一个Bean。

上述XML配置文件指示IoC容器创建3个JavaBean组件,并把id为dataSource的组件通过属性dataSource(即调用setDataSource()方法)注入到另外两个组件中。

在设计上,Spring的IoC容器是一个高度可扩展的无侵入容器。所谓无侵入,是指应用程序的组件无需实现Spring的特定接口。

基本步骤

以一个具体的用户注册登录的例子为例,工程结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring-ioc-appcontext
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── itranswarp
│ └── learnjava
│ ├── Main.java
│ └── service
│ ├── MailService.java
│ ├── User.java
│ └── UserService.java
└── resources
└── application.xml
  1. 导入Spring开发的基本包坐标

pom.xml中引入spring-context依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.itranswarp.learnjava</groupId>
<artifactId>spring-ioc-appcontext</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>

<spring.version>5.2.3.RELEASE</spring.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>
  1. 编写接口和实现类

即主要功能的类与接口。

  1. 创建 Spring核心配置文件
  1. 在Spring配置文件中配置UserDaolmpl

application.xml配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
<?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>

Bean的顺序不重要,Spring根据依赖关系会自动正确初始化。

xml配置文件与如下java代码等价:

1
2
3
UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);
  1. 使用Spring的API获得Bean实例

创建一个Spring的IoC容器实例,然后加载配置文件,让Spring容器为我们创建并装配好配置文件中指定的所有Bean:

1
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

之后,我们就可以从Spring容器中“取出”装配好的Bean然后使用

1
2
UserService userService = context.getBean(UserService.class);
User user = userService.login("bob@example.com", "password");

Spring容器就是ApplicationContext,它是一个接口,有很多实现类,这里我们选择ClassPathXmlApplicationContext,表示它会自动从classpath中查找指定的XML配置文件。

Annotation配置

使用XML配置,写起来非常繁琐,每增加一个组件,就必须把新的Bean配置到XML中。

而Annotation配置,可以无需XML配置文件,让Spring自动扫描Bean并组装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class MailService {
...
}

@Component
public class UserService {

@Autowired
MailService mailService;

public UserService(@Autowired MailService mailService) {
this.mailService = mailService;
}
...
}

@Component注解就相当于定义了一个Bean,有一个可选的名称,默认是即小写开头的类名。

@Autowired相当于把指定类型的Bean注入到指定的字段中。

最后,编写一个AppConfig类启动容器:

1
2
3
4
5
6
7
8
9
@Configuration
@ComponentScan
public class AppConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
...
}
}

AppConfig标注了@Configuration,表示它是一个配置类。实现类AnnotationConfigApplicationContext必须传入一个标注了@Configuration的类名。

@ComponentScan告诉容器,自动搜索当前类所在的包以及子包,把所有标注为@Component的Bean自动创建出来,并根据@Autowired进行装配。

因此要特别注意包的层次结构,工程目录需要如下创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring-ioc-annoconfig
├── pom.xml
└── src
└── main
└── java
└── com
└── itranswarp
└── learnjava
├── AppConfig.java
└── service
├── MailService.java
├── User.java
└── UserService.java

通常来说,启动配置AppConfig位于自定义的顶层包(例如com.itranswarp.learnjava),其他Bean按类别放入子包。

定制Bean

  • Spring默认使用Singleton(单例)创建Bean,容器初始化时创建Bean,容器关闭前销毁Bean。也可指定Scope为Prototype,我们每次调用getBean(Class),容器都返回一个新的实例。
1
2
3
4
5
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
...
}
  • 可将相同类型的Bean注入List。为了指定List中Bean的顺序,可以加上@Order注解:
1
2
3
4
5
6
7
8
9
10
11
@Component
@Order(1)
public class EmailValidator implements Validator {
...
}

@Component
@Order(2)
public class PasswordValidator implements Validator {
...
}
  • 可用@Autowired(required=false)允许可选注入。
1
2
3
4
5
6
7
@Component
public class MailService {
@Autowired(required = false)
// 如果找到一个类型为ZoneId的Bean,就注入,如果找不到,就忽略,而不会抛异常。
ZoneId zoneId = ZoneId.systemDefault();
...
}
  • 可用带@Bean标注的方法创建Bean。返回的Bean仍然是单例。

  • 可使用@PostConstruct@PreDestroy对Bean进行初始化和清理。

首先引入入JSR-250定义的Annotation:

1
2
3
4
5
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>

然后在Bean的初始化和清理方法上标记@PostConstruct@PreDestroy

初始化时间在注入之后。

  • 相同类型的Bean只能有一个指定为@Primary,其他必须用@Quanlifier("beanName")指定别名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@ComponentScan
public class AppConfig {
@Bean("z")
ZoneId createZoneOfZ() {
return ZoneId.of("Z");
}

@Bean
@Qualifier("utc8")
ZoneId createZoneOfUTC8() {
return ZoneId.of("UTC+08:00");
}
}
  • 注入时,可通过别名@Quanlifier("beanName")指定某个Bean;
1
2
3
4
5
6
7
@Component
public class MailService {
@Autowired(required = false)
@Qualifier("z") // 指定注入名称为"z"的ZoneId
ZoneId zoneId = ZoneId.systemDefault();
...
}
  • 可以定义FactoryBean来使用工厂模式创建Bean。

Resource

Spring提供了org.springframework.core.io.Resource类便于注入资源文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class AppService {

@Value("1")
private int version;

@Value("classpath:/logo.txt")
private Resource resource;

private String logo;

@PostConstruct
public void init() throws IOException {
try (var reader = new BufferedReader(
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
this.logo = reader.lines().collect(Collectors.joining("\n"));
}
}

public void printLogo() {
System.out.println(logo);
System.out.println("app.version: " + version);
}
}

类似classpath:/logo.txt表示在classpath中搜索logo.txt文件,然后,我们直接调用Resource.getInputStream()就可以获取到输入流,避免了自己搜索文件的代码。

注入配置

配置文件常用的配置方法是以key=value的形式写在.properties文件中。

@PropertySource可以自动读取配置文件,相较于用Resource来读更简便。

先使用@PropertySource读取配置文件,然后通过@Value${key:defaultValue}的形式注入,可以极大地简化读取配置的麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
@Value("${app.zone:Z}")
String zoneId;

@Bean
ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
return ZoneId.of(zoneId);
}
}

或者先通过一个简单的JavaBean持有所有的配置,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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;
}
}

然后,在需要读取的地方,使用#{smtpConfig.host}注入:

1
2
3
4
5
6
7
8
@Component
public class MailService {
@Value("#{smtpConfig.host}")
private String smtpHost;

@Value("#{smtpConfig.port}")
private int smtpPort;
}

#{}表示从JavaBean读取属性。"#{smtpConfig.host}"的意思是,从名称为smtpConfig的Bean读取host属性。

条件装配

在开发和部署时,我们可能需要的环境并不相同。

创建某个Bean时,Spring容器可以根据注解@Profile来决定是否创建,从而使得应用更加灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@Profile("!test")
ZoneId createZoneId() {
return ZoneId.systemDefault();
}

@Bean
@Profile("test")
ZoneId createZoneIdForTest() {
return ZoneId.of("America/New_York");
}
}

在运行程序时,加上JVM参数-Dspring.profiles.active=test就可以指定以test环境启动。

此外,Spring还提供了@Conditional来进行条件装配。

1
2
3
4
5
@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
...
}

表示如果满足OnSmtpEnvCondition的条件,才会创建SmtpMailService这个Bean。

我们自定义条件为存在环境变量smtp,且值为true

1
2
3
4
5
public class OnSmtpEnvCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "true".equalsIgnoreCase(System.getenv("smtp"));
}
}

Spring Boot提供了更多使用起来更简单的条件注解,例如,如果配置文件中存在app.smtp=true,则创建MailService

1
2
3
4
5
@Component
@ConditionalOnProperty(name="app.smtp", havingValue="true")
public class MailService {
...
}

如果当前classpath中存在类javax.mail.Transport,则创建MailService

1
2
3
4
5
@Component
@ConditionalOnClass(name = "javax.mail.Transport")
public class MailService {
...
}