Spring原理
Bean的作用域
Bean
在Spring IOC和DI阶段,我们学习了Spring是如何帮我们管理对象的。
1.通过@Controller,@Service,@Mapper,@Configuration,@Component ,@Configuration,@Bean等来声明Bean对象。
2.通过ApplicationContext或者BeanFactory来获取对象。
3.通过@Autowired,Setter方法或者构造方法等来为应用程序注入所依赖的Bean对象。
1.通过Bean声明bean,把bean存在Spring容器中
public class Dog {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Component
public class DogBeanConfig {
@Bean
public Dog dog() {
Dog dog = new Dog();
dog.setName("旺财");
return dog;
}
}
2.从Spring容器中获取Bean
@SpringBootApplication
public class SpringBlogBeanApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringBlogBeanApplication.class, args);
Dog dog = context.getBean(Dog.class);
System.out.println(dog);
}
}
也可以在代码中直接注入ApplicationContext的方式来获取Spring容器
@SpringBootTest
class SpringBlogBeanApplicationTest {
@Autowired
private ApplicationContext context;
@Test
void contextLoad() {
Dog dog = context.getBean(Dog.class);
System.out.println(dog);
}
}
修改代码,从Spring容器中多次获取Bean
@SpringBootTest
class SpringBlogBeanApplicationTest {
@Autowired
private ApplicationContext context;
@Test
void contextLoad() {
Dog dog1 = context.getBean(Dog.class);
System.out.println(dog1);
Dog dog2 = context.getBean(Dog.class);
System.out.println(dog2);
}
}
可以看到输出的bean对象地址值是一样的,说明每次从Spring容器中取出来的对象都是同一个。
这也是单例模式,确保一个类只有一个实例,多次创建也不会创建出多个实例。
默认情况下Spring容器中的bean都是单例的,这种行为模式,我们就称为Bean的作用域。
Bean的作用域是指Bean在Spring框架中的某种行为模式。
比如单例作用域:表示Bean在整个Spring中只有一份,他是全局共享的。那么当其他人修改了这个值之后,那么另一个人读取到的就是被修改的值。
修改上述代码,给Dog添加属性name
@SpringBootTest
class SpringBlogBeanApplicationTest {
@Autowired
private ApplicationContext context;
@Test
void contextLoad() {
Dog dog1 = context.getBean(Dog.class);
dog1.setName("艾特");
System.out.println(dog1);
System.out.println(dog1.getName());
Dog dog2 = context.getBean(Dog.class);
System.out.println(dog2);
System.out.println(dog2.getName());
}
}
dog1和dog2为同一个对象,dog2拿到了dog1设置的值。
Bean的作用域
在Spring中支持6种作用域,后4种在Spring MVC环境才生效
1.singleton:单例作用域
2.prototype:原型作用域(多例作用域)
3.request:请求作用域
4.session:会话作用域
5.Application:全局作用域
6.websocket:HTTP WebSocket作用域
作用域 | 说明 |
---|---|
singleton | 每个Spring IoC容器内同名称的bean只有一个实例,但是如果Bean的名称不同那么一个类也可以有多个实例. |
prototype | 每次创建该Bean时都会创建新的实例 |
request | 每个HTTP请求声明周期内,创建新的实例 |
session | 每个HTTP Session生命周期内,创建新的实例 |
Application | 每个ServletContext生命周期内,创建新的实例 |
websocket | 每个WebSocket生命周期内,创建新的实例 |
定义几个不同作用域的Bean
@Component
public class DogBeanConfig {
@Bean
public Dog dog() {
Dog dog = new Dog();
dog.setName("旺财");
return dog;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public Dog singleDog() {
Dog dog = new Dog();
return dog;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Dog prototypeDog() {
Dog dog = new Dog();
return dog;
}
@Bean
@RequestScope
public Dog requestDog() {
Dog dog = new Dog();
return dog;
}
@Bean
@SessionScope
public Dog sessionDog() {
Dog dog = new Dog();
return dog;
}
@Bean
@ApplicationScope
public Dog applicationDog() {
Dog dog = new Dog();
return dog;
}
}
测试不同作用域的Bean取到的对象是否一样
@RestController
public class DogController {
@Autowired
private Dog singleDog;
@Autowired
private Dog prototypeDog;
@Autowired
private Dog requestDog;
@Autowired
private Dog sessionDog;
@Autowired
private Dog applicationDog;
@Autowired
private ApplicationContext applicationContext;
@RequestMapping("/single")
public String single() {
Dog contextDog = (Dog)applicationContext.getBean("singleDog");
return "dog: " + singleDog.toString() + ",contextDog: " + contextDog;
}
@RequestMapping("/prototype")
public String prototype() {
Dog contextDog = (Dog)applicationContext.getBean("prototypeDog");
return "dog: " + prototypeDog.toString() + ",contextDog: " + contextDog;
}
@RequestMapping("/request")
public String request() {
Dog contextDog = (Dog)applicationContext.getBean("requestDog");
return "dog: " + requestDog.toString() + ",contextDog: " + contextDog;
}
@RequestMapping("/session")
public String session() {
Dog contextDog = (Dog)applicationContext.getBean("sessionDog");
return "dog: " + sessionDog.toString() + ",contextDog: " + contextDog;
}
@RequestMapping("/application")
public String application() {
Dog contextDog = (Dog)applicationContext.getBean("applicationDog");
return "dog: " + applicationDog.toString() + ",contextDog: " + contextDog;
}
}
观察Bean的作用域
单例作用域
多次访问,得到的都是同一个对象,并且@Autowired和applicationContext.getBean()也是同一个对象。
多例作用域
观察ContextDog,每次获取的对象都不一样(注入的对象在Spring容器启动时,就已经注入了,所以多次请求也不会发生变化)
请求作用域
每一次请求中,@Autowired和applicationContext.getBean()也是同一个对象。但是每次请求都会重新创建对象。
会话作用域
在一个session中,多次请求,获取到的对象都是同一个
换一个浏览器,发现重新创建对象(另一个session)
Application作用域
在一个应用中,多次访问都是同一个对象
Bean的生命周期
生命周期指的是一个对象从诞生到销毁的整个生命过程,我们把这个过程叫做一个对象的生命周期。Bean的生命周期分为以下5个部分:
1.实例化(为Bean分配内存空间)
2.属性赋值(Bean注入和装配,比如@Autowired)
3.初始化
a.执行各种通知,如BeanNameAware,BeanFactoryAware,ApplicationContextAware的接口方法。
b.执行初始化方法
XML定义init-method
使用注解的方式@PostConstruct
执行初始化后置方法(BeanPostProcessor)
4.使用Bean
5.销毁Bean
a.销毁容器的各种方法,如@PreDestroy,DisposableBean接口方法,destroy-method.
实例化和属性赋值对应构造方法和setter方法的注入。初始化和销毁使用户能自定义扩展的两个阶段,可以在实例化之后,类加载完成之前进行自定义”事件“处理。
Bean的生命周期就好比我们买一栋房子
1.先买房(实例化,从无到有)
2.装修(设置属性)
3.买家电,如冰箱,洗衣机,空调,电视(各种初始化,可以入住)
4.入住(使用Bean)
5.卖房(Bean销毁)
执行流程如下图:
代码演示
@Component
public class BeanLifeComponent implements BeanNameAware {
private Dog singleDog;
public BeanLifeComponent() {
System.out.println("执行构造函数");
}
@Autowired
public void setSingleDog(Dog singleDog) {
this.singleDog = singleDog;
System.out.println("设置属性,执行setSingletonDog....");
}
@Override
public void setBeanName(String name) {
System.out.println("setBeanName方法: " + name);
}
@PostConstruct
public void init() {
System.out.println("执行PostConstruct...");
}
public void use() {
System.out.println("执行use方法...");
}
@PreDestroy
public void destroy() {
System.out.println("执行destroy方法");
}
}
通过运行结果观察
1.先执行构造函数
2.设置属性
3.Bean初始化
4.使用Bean
5.销毁Bean
Spring Boot自动配置
SpringBoot的自动配置就是当Spring容器启动后,一些配置类,bean对象等就自动存入到了IoC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。
SpringBoot自动配置,就是指SpringBoot是如何将依赖jar包中的配置类以及Bean加载到Spring Ioc容器中的。
Spring加载Bean
我们现在来模拟引入第三方的包,其实就是在该项目下,引入第三方的代码,我们采用在该项目下创建不同目录来模拟第三方的代码引入。
现在我们有两个项目目录一个是com.adviser.springblogbean,这个是启动类所在的目录
一个是org.example.demo目录,这个目录模拟第三方包的代码
第三方文件代码
@Component
public class AutoConfig {
public void use() {
System.out.println("执行AutoConfig#use....");
}
}
获取AutoConfig这个Bean,写测试代码
@SpringBootTest
class SpringBlogBeanApplicationTest {
@Autowired
private ApplicationContext context;
@Test
void testAutoConfig() {
AutoConfig bean = context.getBean(AutoConfig.class);
bean.use();
}
}
运行程序
观察日志:No qualifying bean of type 'org.example.demo.AutoConfig' available
没有org.example.demo.AutoConfig这个类型的Bean
Spring通过五大注解和@Bean注解可以帮助我们把Bean加载到SpringIoC容器中,以上有个前提就是这些注解类需要和SpringBoot启动类在同一个目录下(@SpringBootApplication标注的类就是SpringBoot项目的启动类)
启动类的目录为com.adviser.springblogbean,而AutoConfig这个类在org.example.demo下,所以SpringBoot没有扫描到。
但是当我们引入第三方的Jar包时,第三方的Jar代码目录肯定不在启动类的目录下,如何告诉Spring帮我们管理这些Bean呢?
我们需要指定路径或者引入文件,告诉Spring,让Spring进行扫描到
常见的两种解决方法:
1.@ComponentScan组件扫描
2.@Import导入(使用@Import导入的类会被Spring加载到IoC容器中)
1.@ComponentScan
通过@ComponentScan注解,指定Spring扫描路径
@ComponentScan("org.example.demo")
@SpringBootApplication
public class SpringBlogBeanApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringBlogBeanApplication.class, args);
BeanLifeComponent beanLifeComponent = context.getBean(BeanLifeComponent.class);
beanLifeComponent.use();
}
}
可以指定扫描多个包
@ComponentScan({"org.example.demo", "com.adviser.springblogbean"})
运行程序
可以看到,这次AutoConfig Bean获取到了
但是很明显,Spring没有采用这种方式,比如我们引入第三方框架时,没有加扫描路径,比如MyBatis。如果我们Spring Boot采用这种方式,当我们引入大量的第三方依赖,比如MyBatis,jackson等时,就需要在启动类上配置不同依赖需要扫描的包,这种方式会非常繁琐。
2.@Import
@Import导入主要有以下几种方式
1.导入类
@Import(AutoConfig.class)
@SpringBootApplication
public class SpringBlogBeanApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBlogBeanApplication.class, args);
}
}
可以看到这种方式也能告诉Spring加载biteConfig
但是又多了一些配置项
@Component
public class AutoConfig2 {
public void use() {
System.out.println("执行AutoConfig#use....");
}
}
我们可以采用导入多个类
@Import({AutoConfig.class, AutoConfig2.class})
@SpringBootApplication
public class SpringBlogBeanApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBlogBeanApplication.class, args);
}
}
但是很明显,这种方式也很繁琐,所以Spring Boot依然没有采用
2.导入ImportSelector接口实现类
ImportSelector接口实现类
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//需要导入全限定类名
return new String[]{"org.example.demo.AutoConfig", "org.example.demo.AutoConfig2"};
}
}
启动类
@Import({MyImportSelector.class})
@SpringBootApplication
public class SpringBlogBeanApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBlogBeanApplication.class, args);
}
}
运行程序:
可以看到,我们采用这种方式也可以导入第三方依赖提供的Bean
但是这会出现一个很明显的问题:就是使用者需要知道第三方依赖中有哪些Bean对象或配置类。如果漏掉其中的一些Bean,就可能导致我们的项目出现大的事故。
依赖中有很多Bean,使用时需要配置哪些bean,第三方依赖最清楚,那能否由第三方依赖来做这件事?
比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都是以@EnableXXXX开头的注解,注解中封装的就是@Import注解
1.第三方依赖提供注解
package org.example.demo;
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 EnableAutoConfig {
}
注解中封装@Import注解,导入MyImportSelector.class
2.在启动类上使用第三方提供的注解
@EnableAutoConfig
@SpringBootApplication
public class SpringBlogBeanApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBlogBeanApplication.class, args);
}
}
3.运行程序
可以看到,这种方式也可以导入第三方依赖提供的Bean。
并且这种方式更优雅一点。SpringBoot采用的也是这种方式
SpringBoot原理分析
1.源码阅读
SpringBoot是如何帮我们做的呢?这一切的来自SpringBoot的启动类开始
SpringBootApplication标注的类就是SpringBoot项目的启动类
@SpringBootApplication
public class SpringBlogBeanApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBlogBeanApplication.class, args);
}
}
这个类和普通类唯一的区别就是@SpringBootApplication注解,这个注解也是SpringBoot实现自动配置的核心
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//代码省略......
}
SpringBootApplication是一个组合注解,注解中包含了:
1.元注解
JDK中提供了4个标准的用来对注解类型进行注解的注解类,我们称之为meta-annotation(元注解),它们分别是:
@Target:描述注解的使用范围(即被修饰的注解可以用在什么地方)
@Retention:描述注解保留的时间范围
@Documented:描述在使用javadoc工具为类生成帮助文档时是否要保留其注解信息
@Inherited:使被它修饰的注解具有继承性(如果某个类使用了被@Inherited修饰的注解,则其子类将自动具有该注解)
2.@SpringBootConfiguration
里面就是@Configuration,标注当前类为配置类,其实只是做了一层封装改了个名字而已。
@Indexed注解,是用来加速应用启动的,不用关心)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
3.@EnableAutoConfiguration(开启自动配置)
Spring自动配置的核心注解,下面详细讲解
4.@ComponentScan(包扫描)
可以通过basePackageClasses或basePackages来定义要扫描的特定包,如果没有定义特定的包,将从声明该注解的类的包开始扫描,这也就是为什么SpringBoot项目声明的注解类必须要在启动类的目录下。
默认情况下com.adviser.springautoconfig包及其子包会被扫描到,而com.adviser.MyOtherService包下的类即使加了@Service等注解交给Spring管理,那么也会因为Spring不会扫描这个包而导致Spring容器中找不到对应的Bean。
excludeFilters自定义过滤器,通常用于排除一些类,注解等。
@EnableAutoConfiguration详解
看下@EnableAutoConfiguration注解的实现:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
这个注解包含两部分:
@Import(AutoConfigurationImportSelector.class)
使用@Import注解,导入了实现importSelector接口的实现类
public class AutoConfigurationImportSelector implements
DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
//获取⾃动配置的配置类信息
AutoConfigurationEntry autoConfigurationEntry =
this.getAutoConfigurationEntry(annotationMetadata);
return
StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
}
selectImport()方法底层调用了getAutoConfigurationEntry()方法,获取可自动配置的配置类信息集合。
点进去
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
getAutoConfigurationEntry()方法通过调用getCandidateConfigurations(annotationMetadata, attributes)方法获取在配置文件中配置的所有自动配置类的集合
点进去:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
.getCandidates();
Assert.notEmpty(configurations,
"No auto configuration classes found in "
+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
以前的版本
protected List < String > getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List < String > configurations = new
ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderF actoryClass(), this.getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class,
this.getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations, "No auto configuration classes found in
META - INF / spring.factories nor in META INF / spring / org.springframework.boot.autoconfigure.AutoConfiguration.imports.If you are using a custom packaging, make sure that file is correct.
");
return configurations;
}
getCandidateConfigurations方法的功能:获取所有基于META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,META-INF/spring.factories⽂件中配置类的集合。
在引入的起步依赖中,通常都有包含以上两个文件
META-INF/spring.factories⽂件是Spring内部提供的一个约定俗成的加载方式,只需要在模块的 META-INF/spring.factories文件中配置即可,Spring就会把相应的实现类注入到Spring容器中。
注:回家再所有jar包下的classpath路径下的 META-INF/spring.factories⽂件,这样的文件不止一个。
2.@AutoConfigurationPackage
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
这个注解主要是导入一个配置文件AutoConfigurationPackages.Registrar.class
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
Registrar实现了ImportBeanDefinitionRegistrar接口,就可以被注解@Import导入到spring容器里。
new PackageImports(metadata).getPackageNames().toArray(new String[0]):当前启动类所在包名。
所以@AutoConfigurationPackage就是将启动类所在的包下面所有的组件都扫描注册到spring容器中。
总结
SpringBoot自动配置原理的大概流程如下: