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
|
- 导入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>
|
- 编写接口和实现类
即主要功能的类与接口。
- 创建 Spring核心配置文件
- 在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);
|
- 使用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) 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 zoneId = ZoneId.systemDefault(); ... }
|
首先引入入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") 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") 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 { ... }
|