1. Bean的作用域
Bean在Spring中表示的是Spring管理的对象,Bean的作用域是只Bean在Spring框架中的某种行为模式。
在Spring中,支持6中作用域:
- singleton:单例作用域,在整个 Spring IoC 容器中,只创建一个 Bean 实例。这是默认的作用域。
- prototype:原型作用域,每次获取 Bean 时都会创建一个新的实例。
- request:请求作用域,在一次 HTTP 请求中,创建一个 Bean 实例。该作用域仅适用于 WebApplicationContext 环境。
- session:会话作用域,在一次 HTTP Session 中,创建一个 Bean 实例。同样仅适用于 WebApplicationContext 环境。
- application:每个ServletContext生命周期内创建新的实例。
- websocket:HTTP WebSocket作用域。
后面4种在Spring MVC环境种才生效
1.1 singleton
单例模式,在整个Spring IoC 容器中,只创建一个Bean的实例:
@Configuration
public class DogConfig {
@Bean
public Dog dog() {
Dog dog = new Dog();
dog.setName("小花");
dog.setColor("黑白相间");
return dog;
}
}
以默认作用域注入一个Dog对象,通过Spring上下文/@Autowired 获取Bean:
@SpringBootTest
class J20240524SpringTheoryApplicationTests {
@Autowired
ApplicationContext context;
@Test
void contextLoads() {
Dog dog1 = context.getBean("dog", Dog.class);
Dog dog2 = context.getBean("dog", Dog.class);
System.out.println(dog1);
System.out.println(dog2);
}
}
运行结果:
1.2 prototype
原型作用域,每次获取 Bean 时都会创建一个新的实例。
使用@Scope注解来指定Bean的作用域:
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Dog propertyDog() {
return new Dog();
}
@Test
void contextLoads1() {
Dog dog1 = context.getBean("propertyDog", Dog.class);
Dog dog2 = context.getBean("propertyDog", Dog.class);
System.out.println(dog1);
System.out.println(dog2);
}
运行结果:
1.3 request
请求作用域,在一次 HTTP 请求中,创建一个 Bean 实例。
添加一个request作用域的Bean
@Bean
@RequestScope
public Dog requestDog() {
return new Dog();
}
这里的@RequestScop注解相当于 @Scope("request")
@RequestMapping("/request")
public String request() {
Dog dog1 = context.getBean("requestDog", Dog.class);
Dog dog2 = context.getBean("requestDog", Dog.class);
return "Autowired: " + dog1.toString() + "<br/>" + "context: " + dog2.toString();
}
启动程序访问url:
可以看到一次请求中两次获取的Bean是同一个,当我们刷新页面(重新发送请求):
获取到的Bean和上次不同
1.4 session
会话作用域,在一次 HTTP Session 中,创建一个 Bean 实例。同样仅适用于 WebApplicationContext 环境。
@Bean
@SessionScope
public Dog sessionDog() {
return new Dog();
}
@RequestMapping("/session")
public String session() {
Dog dog1 = context.getBean("sessionDog", Dog.class);
Dog dog2 = context.getBean("sessionDog", Dog.class);
return "Autowired: " + dog1.toString() + "<br/>" + "context: " + dog2.toString();
}
运行程序访问url:
即便我们重新访问,得到的也是同一个对象,因为请求在一个会话中,我们可以使用另一个浏览器来访问,此时服务器会认为这是两个不同的会话就能得到不同的对象:
1.5 application
在一个应用中,多次访问都是同一个对象:
即使在不同会话中访问的也是同一个对象,和singleton作用域有些类似,但也有不同之处:
一个web容器中可能有多个应用程序,singleton作用域是针对于整个web容器来说只能有一个对象,application作用域则是对于每个应用程序中只能有一个对象。
2. Bean的生命周期
生命周期指的是一个对象从诞生到销毁的整个生命过程,我们把这个过程叫做一个对象的生命周期,Bean的生命周期分为以下5个部分:
- 实例化:为Bean分配内存空间。
- 属性赋值:初始化属性,Bean注入和装配。
- 初始化:a.执行各种通知,如BeanNameAware,BeanFactoryAware的接口方法。b.执行初始化方法,初始化后置方法。
- 使用Bean
- 销毁Bean
初始化和销毁使用用户自定义扩展的两个阶段,可以在实例化之后,类加载之前进行自定义处理。
@Component
public class DogComponent implements BeanNameAware {
public Dog singletonDog;
public DogComponent() {
System.out.println("执行构造方法实例化");
}
@Autowired
public void setSingletonDog(Dog singletonDog) {
this.singletonDog = singletonDog;
System.out.println("执行setSingletonDog注入Bean");
}
@Override
public void setBeanName(String name) {
System.out.println("执行setBeanName, BeanName: " + name);
}
@PostConstruct
public void init() {
//@PostConstruct:表示该方法为初始化方法
System.out.println("执行init方法初始化");
}
public void use() {
System.out.println("执行use方法");
}
@PreDestroy
public void destroy() {
System.out.println("执行destroy方法, 进行销毁前处理");
}
}
@SpringBootTest
class DogComponentTest {
@Autowired
public DogComponent component;
@Test
void use() {
component.use();
}
}
执行测试代码:
3. Spring Boot 自动配置
Spring Boot 自动配置就是当Spring容器启动后一些配置类,Bean对象等就自动存入到了IoC容器中,不需要手动去声明从而简化开发。
3.1 Spring 加载 Bean
通过我们之前的学习,我们知道,要把一个对象加载到IoC容器中需要给对应的类添加上五大注解或者使用@Bean注解,但是当我们想要把第三方的类对象添加到IoC容器中时,我们是无法给第三方的代码中添加注解的,我们之前的解决方案是在本地实现一个类,然后通过@Bean注解来添加,但是我们并不知道使用的第三方包需要添加哪些Bean,所以这样实现显然是不太科学的。
解决方案:
使用第三方jar包,需要配置哪些Bean,一定是第三方作者最清楚,那么就让第三方来完成添加Bean的操作:
package com.test.autoconfig;
public class CatConfig1 {
public void use() {
System.out.println("useCatConfig1");
}
}
package com.test.autoconfig;
public class CatConfig2 {
public void use() {
System.out.println("useCatConfig2");
}
}
假设com.test.autoconfig 是一个第三方包,这个第三方包中可以定义一个实现了ImportSelector接口的类在这个类中设置要添加哪些Bean:
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {
"com.test.autoconfig.CatConfig1",
"com.test.autoconfig.CatConfig2"
};
}
}
然后通过@Import注解添加到启动类上:
使⽤@Import导入的类会被Spring加载到IoC容器中
@Import(com.test.autoconfig.MyImportSelector.class)
@SpringBootApplication
public class J20240524SpringTheoryApplication {
public static void main(String[] args) {
SpringApplication.run(J20240524SpringTheoryApplication.class, args);
}
}
但是这种形式需要使用者记得该jar包的哪个类是配置类,也不太方便,现在比较常见的方案是第三方提供一个注解,这个注解一般都是以@EnableXXX开头的注解,注解中封装@Import注解:
package com.test.autoconfig;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableCat {
}
然后在启动类上加上该注解即可:
@EnableCat
@SpringBootApplication
public class J20240524SpringTheoryApplication {
public static void main(String[] args) {
SpringApplication.run(J20240524SpringTheoryApplication.class, args);
}
}
3.2 SpringBoot 源码分析
上面我们简单讲解了原理,接下来我们通过SpringBoot的源码来看看Spring是如何具体实现的。
在Spring项目的启动类上有一个@SpringBootApplication注解,显然这个注解就是关键所在,我们转到该注解的定义:
其中@ComponentScan注解我们知道它的功能是指定加载Bean时的扫描路径,但是这里并没有指定所以扫描路径是使用该注解的地方,也就是启动类所在目录和子目录。
再看@SpringBootConfiguration注解,我们转到其定义:
可以看到有五大注解的@Configuration 注解, 所以这个注解就是一个扩展了的@Configuration 注解,主要作用是把类交给Spring。
现在除了元注解就只剩下@EnableAutoConfiguration 注解,显然这个注解就是关键所在
最后再看@EnableAutoConfiguration 注解:
在这个注解中除了元注解,我们可以看到两个注解,我们重点关注这两个注解
3.2.1 AutoConfigurationImportSelector
@Import注解是用来导入Bean的,这个注解导入了一个Auto ConfigurationImportSelector类,我们转到这个类的实现:
在这个类中我们可以看到它实现了ImportSelector接口的 selectImports方法用来添加Bean,我们继续转到图中getAutoConfigurationEntry()方法的实现,显然该方法的作用是获取要导入的Bean的类的全限定名称:
仔细观看这个方法,发现该方法主要都是围绕configurations这个变量来处理的,所以我们直接点开getCandidateConfigurations()方法,查看configurations是如何生成的:
注意该方法的第三行,这里做了一个判断,如果configurations是空的,就会提示下面绿色字符串,这句话翻译过来的意思是:
在META-INF/spring/org.springframework.boot.autoconfigurationAutoConfiguration.中找不到自动配置类。如果您使用的是自定义包装,请确保该文件是正确的。
显然Bean的加载和META-INF/spring/org.springframework.boot.autoconfigurationAutoConfiguration.这个路径下的文件有关,我们双击shift查找该文件
在该文件中我们发现了非常多的全限定类名,显然该文件中存储了使用springBoot 需要添加的Bean的全限定类名,我们随便打开一个其中的类:
发现该类中就通过@Bean注解声明了一些需要交给Spring的Bean,这里我们发现,里面的类名都有红色的报错信息,这是因为我们没有引入对应的依赖,所以是找不到对应的类的。但是这个文件又是SpringBoot中默认带有的,也就是Spring会默认加载这个Bean,但是我们没有引入对应的jar包却没有报错,这是因为@ConditionalOnMissingBean注解,该注解会根据条件来决定是否要加载使用该注解的类中提供的Bean,如果没有引入对应的jar包自然也就不会加载,所以不会报错。
以上的流程是Spring加载默认Bean的方式/过程,但是问题又来了,如果一些Spring没有默认加载的第三方库也需要Spring来管理一些Bean呢?我们回到getAutoConfigurationEntry方法:
我们发现倒数第二行还有一个方法,通过该方法的名称我们猜测它也是自动配置相关功能的,我们转到实现:
继续转到实现:
最后来到这里:
我们发现,Spring又根据META-INF/spring.factories路径下的这个文件又加载了一些Bean,在Spring项目启动时,Spring会扫描所有的META-INF/spring.factories,包括第三方的jar包,也就是说,只要想让Spring管理Bean的第三方包,只需添加一个spring.factories文件在META-INF下,就可以让Spring来管理其中包含的类。
3.2.2 @AutoConfigurationPackage
在@EnableAutoConfiguration注解中,还有一个@AutoConfigurationPackage注解,我们转到实现:
该注解导入了一个类我们转到实现:
其中有一个名为registerBeanDefinitions的方法,如果使用debug查看,会发现这里传入的包是启动类的包。
它的作用是指定自动配置包的扫描起始位置,确保Spring Boot能够正确扫描到自动配置类并实现自动配置功能,即确保@Import(AutoConfigurationImportSelector.class)能被SpringBoot扫描到。