JavaEE Learning Note - Spring

Spring Framework

Spring是一个支持快速开发Java EE应用程序的框架。它提供了一系列底层容器和基础设施,并可以和大量常用的开源框架无缝集成,可以说是开发Java EE应用程序的必备。

IoC容器

IoC原理

Spring提供的容器又称为IoC容器(Inversion of Control),又称为依赖注入(DI:Dependency Injection)。它解决了一个最主要的问题,将组件的创建和配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

IoC容器要负责实例化所有的组件,可以通过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>

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

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

依赖注入可以通过set()方法实现。但依赖注入也可以通过构造方法实现。比如:

1
2
3
4
5
6
7
public class BookService {
private DataSource dataSource;

public BookService(DataSource dataSource) {
this.dataSource = dataSource;
}
}

Spring的IoC容器同时支持属性注入和构造方法注入,并允许混合使用。

Bean装配

导入依赖到项目,编辑pom.xml:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.11</version>
</dependency>

编写要使用的类:

AService:

1
2
3
4
5
6
7
8
9
10
11
12
public class AService {
int val;
public void setVal(int val){
this.val=val;
}
public AService getaService() {
return aService;
}
public void run(){
System.out.println(val+1);
}
}

BService:

1
2
3
4
5
6
7
8
9
public class BService {
private AService aService;
public void setaService(AService aService){
this.aService=aService;
}
public void run(){
System.out.println(aService.val);
}
}

编写application.xml(/src/main/resources/application.xml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="bService" class="web.com.BService">
<property name="aService" ref="aService" />
</bean>

<bean id="aService" class="web.com.AService">
<property name="val" value="1"/>
</bean>
</beans>
  • 每个<bean ... >都有一个id标识,相当于Bean的唯一ID
  • BService中,通过<property name="..." ref="..." />注入了另一个Bean
  • 通过<property name="..." value="...">注入其他数据类型(booleanintString
  • Bean的顺序不重要,Spring会根据依赖关系自动正确执行

Spring容器会读取该XML文件后使用反射完成,相当于执行了:

1
2
3
AService aService=new AService();
BService bService=new BService();
bService.setaService(aService);

为了让Spring识别并读取配置文件,我们需要创建Spring的IoC容器实例:

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

之后便可以从容器中调用并使用:

1
2
3
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
BService bService=context.getBean(BService.class);
AService aService=bService.getaService();

用Annotation配置

可以给类添加一个注解@Component,相当于定义了一个Bean,让Spring自动扫描并组装。

注解@Autowired就相当于把指定的Bean注入到指定的字段中,大幅简化了注入,不仅可以写在set()上,还可以直接写在字段甚至构造方法中。

1
2
3
4
5
6
7
8
@Component
public class UserService {
MailService mailService;
public UserService(@Autowired MailService mailService) {
this.mailService = mailService;
}
//...
}

注解@Configuration表示为一个配置类,还可以使用@ComponentScan让容器自动搜索当前类所在包以及子包,把所有标注@Component的Bean自动创建出来,并根据@Autowired装配

1
2
3
4
5
6
7
8
9
10
@Configuration
@ComponentScan
public class AppConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
User user = userService.login("bob@example.com", "password");
System.out.println(user.getName());
}
}

定制Bean

对Spring容器来说,将Bean标记为@Component后,会自动创建一个单例,即容器初始化时创建,关闭前销毁,调用getBean(class)获取到的总是同一个实例。

但如果我们添加一个额外的@Scope注解,我们每次调用@getBean(class)容器都会返回一个新的实例,这种Bean被称为Prototype(原型),例如:

1
2
3
4
5
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class BService {
//...
}

我们有时候会有一系列接口相同,不同实现类的Bean。例如对email、password、name三个变量镜进行验证,我们可以先定义验证接口:

1
2
3
public interface Validator{
void validate(String email,String password,String name);
}

然后分别三个Validator进行验证:

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
@Component
public class EmailValidator implents 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作为入口进行验证:

1
2
3
4
5
6
7
8
9
10
11
@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);
}
}
}

Validators被注入了一个List<Validator>,Spring会自动将所有类型的Validator装配为一个List注入进来,每新增一个Validator就会被自动装配进去。

因为Spring是通过扫描classpath获取到所有的Bean,而List是有序的,要指定List中Bean的顺序,可以加上@Order()注解指定顺序:

1
2
3
4
5
@Component
@Order(1)
public class EmailValidator implements Validator {
//...
}

当我们标记一个@Autowired后,Spring如果没找到对应的Bean,会抛出NoSuchBeanDefinitionException可以增加一个required=false参数表示找不到就忽略,非常适合有定义就用定义,没定义就用默认值的情况。即:@Autowired(required=false)

当我们需要创建一个不在当前package的Bean时,我们需要自己在@Configuration类中编写一个方法创建并返回它,此方法也需要标记@Bean注解。Spring对@Bean的方法只调用一次,因此返回的Bean仍然是单例。

有时Bean在注入后需要进行初始化,关闭时要清理资源,我们通常会定义一个init()方法初始化,shutdown()方法进行清理,引入JSR-250定义的Annotation:

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

并在初始化和清理方法上标记PostConstruct@PreDestroy

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class Bean {
@Autowired(required = false) Count count;
@PostConstruct
public void init(){
System.out.println("Init");
}
@PreDestroy
public void shutdown(){
System.out.println("Shutdown");
}
}

Spring只根据注解Annotation查找无参数方法,对方法名不作要求。

有时,对于同一种类型的Bean,我们需要创建多个实例,我们需要给他们添加不同的别名,防止出现重复的Bean定义。

可以用@Bean("name")或者@Bean+@Qualifier("name")指定别名

注入Bean时也需要@Qualifier("name")指定注入的名称:

1
2
3
@Autowired
@Qualifier("z") // 指定注入名称为"z"的Counter
Counter count=Counter.getDefault();

还可以将某个Bean指定为@Primary在注入时,如果没有指出Bean的名字,Spring会注入标记有@Primary的Bean

使用Resource