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="...">
注入其他数据类型(boolean
、int
、String
)
- 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) 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") Counter count=Counter.getDefault();
|
还可以将某个Bean指定为@Primary
在注入时,如果没有指出Bean的名字,Spring会注入标记有@Primary
的Bean
使用Resource