[Spring] Spring原理(SpringBoot完结)

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343

🏵️热门专栏:

🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482

🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482

🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482

🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482

🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482

🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482

感谢点赞与关注~~~

目录

  • [1. Bean的作用域](#1. Bean的作用域)
    • [1.1 概念](#1.1 概念)
    • [1.2 Bean的作用域](#1.2 Bean的作用域)
  • [2. Bean的生命周期](#2. Bean的生命周期)
    • [2.1 概念](#2.1 概念)
    • [2.2 代码演示](#2.2 代码演示)
  • [3. SpringBoot自动配置](#3. SpringBoot自动配置)
    • [3.1 Spring加载Bean](#3.1 Spring加载Bean)
      • [3.1.1 问题描述](#3.1.1 问题描述)
      • [3.1.2 原因分析](#3.1.2 原因分析)
      • [3.1.3 解决方案](#3.1.3 解决方案)
    • [3.2 SpringBoot原理分析](#3.2 SpringBoot原理分析)
      • [3.2.1 源码](#3.2.1 源码)
      • [3.2.2 @EnableAutoConfiguratio详解](#3.2.2 @EnableAutoConfiguratio详解)
      • [3.2.3 总结](#3.2.3 总结)

1. Bean的作用域

1.1 概念

在我们学习SpringIOC和DI阶段的时候,我们学习了Spring是如何帮助我们管理对象的.

  1. 首先我们可以通过@Controller ,@Service ,@Repository ,@Component ,@Configuration ,@Bean 来声明Bean对象.
  2. 通过AppliactionContext(Spring上下文)或者BeanFactory来获取对象.
  3. 通过@Autowired,Setter方法或者构造方法来为应用程序注入所依赖的Bean对象.
    可以通过https://blog.csdn.net/2301_80050796/article/details/140531072?spm=1001.2014.3001.5501这篇文章来复习.
    创建一个类,叫做Dog类.
java 复制代码
@Data
@Getter
public class Dog {
    @Setter
    public Integer age;
    @Setter
    public String name;
}

之后在DogBeanConfig类中创建一个对象,之后把对象放入IoC容器中.

java 复制代码
@Component
public class DogBeanConfig {
    @Bean
    public Dog dog(){
        Dog dog = new Dog();
        dog.setAge(2);
        dog.setName("哈士奇");
        return dog;
    }
}

之后从上下文中两次拿到dog对象.

java 复制代码
@Test
void dogTest(){
    Dog dog1 = applicationContext.getBean(Dog.class);
    System.out.println(dog1);
    Dog dog2 = applicationContext.getBean(Dog.class);
    System.out.println(dog2);
}

这里我们发现,Bean对象的输出地址是一样的,说明每次从IoC容器中取出来的是同一个对象 .

这也就是单例模式,在默认的情况下,Spring中的Bean都是单例的,这种共行为模式,我们就称为Bean的作用域.Bean的作用域指的是Bean在Spring框架中的某种行为模式.

比如单例模式的作用域:表示在整个Spring中,它只有一个,是全局共享的,也就是当有别人修改它的值之后,另一个人读到的就是被修改的值.

java 复制代码
@Test
    @Test
    void dogTest(){
        Dog dog1 = applicationContext.getBean(Dog.class);
        dog1.setName("金毛");
        System.out.println(dog1);
        System.out.println(dog1.getName());
        Dog dog2 = applicationContext.getBean(Dog.class);
        System.out.println(dog2);
        System.out.println(dog2.getName());
    }

观察运行结果,我们发现修改之后的Bean对象拿到名字的时候是修改之后的名字.

那么能不能将Bean对象设置为非单例的呢(每次获取Bean对象都是一个新对象)?我们这时候就要提到Bean的作用域了.

1.2 Bean的作用域

在Spring中支持6种作用域,其中后4种在SpringMVC环境中才会生效.

  1. singleton:单例作用域,表示的是在每个IoC容器中,同名称的Bean只有一个
  2. prototype:原型作用域(多例作用域),每次使用该Bean的时候,都会创建一个新实例.
  3. request:请求作用域,每次http请求都会创建一个新的实例
  4. session:会话作用域,每一个http session周期之内,都会创建一个新实例.
  5. Application:全局作用域,每个ServletContext生命周期内,创建新的实例
  6. websocket:HTTPWebSocket作用域,每个WebSocket生命周期内,创建新的实例
    指定Bean对象的生命周期,我们可以通过@Scope(翻译:范围)注解来指定.我们来定义几个作用域不同的Bean.
java 复制代码
@Component
public class DogBeanConfig {
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Dog singleDog(){
        Dog dog = new Dog();
        dog.setAge(1);
        dog.setName("萨摩耶");
        return dog;
    }
    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Dog prototypeDog(){
        Dog dog = new Dog();
        dog.setAge(3);
        dog.setName("柴犬");
        return dog;
    }
    @Bean
    @RequestScope
    public Dog requestDog(){
        Dog dog = new Dog();
        dog.setAge(2);
        dog.setName("哈士奇");
        return dog;
    }
    @Bean
    @SessionScope
    public Dog sessionDog(){
        Dog dog = new Dog();
        dog.setAge(4);
        dog.setName("柯基");
        return dog;
    }
}

测试不同的Bean获取的对象是否一样.

java 复制代码
@RestController
public class DogController {
	@Autowired
	private Dog singleDog;
	@Autowired
	private Dog prototypeDog;
	@Autowired
	private Dog requestDog;
	@Autowired
	private Dog sessionDog;
	@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.toString();
	}
	@RequestMapping("/session")
	public String session(){
		Dog contextDog = (Dog)applicationContext.getBean("sessionDog");
		return "dog:"+sessionDog.toString()+",contextDog:"+contextDog.toString();
	}
	@RequestMapping("/application")
	public String application(){
		Dog contextDog = (Dog)applicationContext.getBean("applicationDog");
		return "dog:"+applicationDog.toString()+",contextDog:"+contextDog.toString();
	}
}

我们每次请求有两个对象,一个是通过@Autowired注入的Bean对象,一个是通过上下文获取的Bean对象.
单例作用域 :

每一次访问都是同一个对象,并且@Autowired 和applicationContext.getBean() 也是同⼀个对象.

原型作用域 :

从上下文获取的Bean对象每一次请求都不一样,但是通过注解注入的Bean对象没有改变,这是由于在项目运行起来之后就已经注入完成了,所以多次请求也不会发生变化.

请求作用域 :

每一次请求中,@AutowiredapplicationContext.getBean() 也是同⼀个对象.每次请求都会创建一个新对象.

会话作用域 :

在一个session中,多次请求,获取到的对象都是同一个.换一个浏览器访问就会重新创建对象.


Appliaction作用域 :

在⼀个应用中,多次访问都是同⼀个对象

这个Appliaction的效果和Singleton有些类似,但是他们有所不同,区别在于:Application scope是ServletContext的单例,singleton是⼀个ApplicationContext的单例.在⼀个web容器中ApplicationContext可以有多个,但是一个web容器中只有一个ServletContext容器.(了解)

2. Bean的生命周期

2.1 概念

生命周期指的是一个对象从诞生到销毁的过程.

Bean的生命周期分为一下5个部分:

  1. 实例化(为Bean分配内存空间) ---> 构造方法
  2. 属性赋值(Bean注入和装配,比如@Autowired) ---> Setter方法注入
  3. 初始化: 就是执行通知,BeanNameAware,BeanFactoryAware,ApplicationContextAware的接口方法和执行使用注解@PostConstruct 修饰的初始化方法.
  4. 使用Bean.
  5. 销毁Bean: 就是执行销毁容器DisposableBean 接口方法和执行使用注解@PreDestroy修饰的销毁容器的方法.

这个就好比我们想买一套房子:

  1. 需要先买房(实例化,分配内存空间)
  2. 装修,把毛坯房变为精装房(执行属性赋值,执行@Autowired注入赋值)
  3. 购买家电.(执行初始化方法,包括接口实现和注释修饰)
  4. 拎包入住(使用Bean)
  5. 寿命到期,拆迁(销毁Bean)

2.2 代码演示

java 复制代码
@Component
public class BeanLifeComponent implements BeanNameAware {
    private Dog dog;
    public BeanLifeComponent(){
        System.out.println("实例化Bean...");
    }
    @Autowired
    public void setDog(Dog dog1) {//set方法注入法
        this.dog = dog1;
        System.out.println("属性赋值Bean");
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("执行了BeanNameAware接口的通知方法(初始化Bean)");
    }
    @PostConstruct
    public void init(){
        System.out.println("执行了PostConstruct注解修饰的方法(初始化Bean)");
    }
    public void use(){
        System.out.println("使用Bean");
    }
    @PreDestroy
    public void destroy(){
        System.out.println("销毁Bean");
    }
}

测试代码:

java 复制代码
@Test
void beanLifeComponent(){
    BeanLifeComponent beanLifeComponent = applicationContext.getBean(BeanLifeComponent.class);
    beanLifeComponent.use();
}

测试结果如下:

即使类中的这些方法变换了顺序,也不会改变这些这些内容输出的顺序.

3. SpringBoot自动配置

SpringBoot的自动配置就是当Spring容器启动后,⼀些配置类,bean对象等就自动存入到了IoC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作.

3.1 Spring加载Bean

3.1.1 问题描述

现在有一个问题,就是属于Spring本身的类自动导入还可以理解,但是如果是第三方的库想要导入Spring是如何做到的呢?

现在我们使用在项目下创建不同的目录来模拟第三方代码的引入.
数据准备:

  1. com.jrj.springprincipledemo软件包之外新创建一个软件包spring_autoconfig,之后在spring_autoconfig引入第三方代码TestConfig.
java 复制代码
@Component
public class TestConfig {
    public void print(){
        System.out.println("打印...");
    }
}
  1. com.jrj.springprincipledemo目录下的运行类中通过上下文来获取这个Bean对象,我们发现是获取不到的.
java 复制代码
@SpringBootApplication
public class SpringPrincipleDemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringPrincipleDemoApplication.class, args);
        TestConfig testConfig = context.getBean(TestConfig.class);
        System.out.println(testConfig);
    }
}

3.1.2 原因分析

根据前面学过的知识,我们知道,这是由于启动项只能扫描到他所在的目录以及子目录造成的,而在外面的目录却扫描不到.

当我们引入第三方的jar包的时候,第三方jar代码目录一定不再启动类的目录下,那么如何告诉Spring帮我们管理这些Bean呢?

3.1.3 解决方案

我们需要指定路径或者引入文件,告诉Spring,Spring扫描到.

常见的解决方案有两种:

  1. @ComponentScan注解添加扫描路径.
    这种方法我们之前在SpringIoC&DI注入介绍过,不再赘述
  2. @Import注解导入
    @Import导入形式主要有以下两种:
    • 导入类
java 复制代码
@SpringBootApplication
@Import(TestConfig.class)
public class SpringPrincipleDemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringPrincipleDemoApplication.class, args);
        TestConfig testConfig = context.getBean(TestConfig.class);
        System.out.println(testConfig);
    }
}

我们看到了启动类成功拿到了IoC容器中的Bean对象.

也可以使用大括号导入多个类:@Import({TestConfig1.class,TestConfig2.class})

  • 导入ImportSelector接口实现类.
java 复制代码
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"spring_autoconfig.TestConfig"};
    }
}

需要注意的一点是,给返回值导入类的时候,需要导入类的全限定名称.

在启动类的上面直接导入实现ImportSelector的类.

java 复制代码
@SpringBootApplication
@Import(MyImportSelector.class)
public class SpringPrincipleDemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringPrincipleDemoApplication.class, args);
        TestConfig testConfig = context.getBean(TestConfig.class);
        System.out.println(testConfig);
    }
}

依然可以获取到对象:

问题:但是他们都有⼀个明显的问题,就是使用者需要知道第三方依赖中有哪些Bean对象或配置类.依赖中有哪些Bean,使用的时候需要配置哪些Bean,只有第三方最清楚,能否让第三方来做这件事情呢?

  • 比较常见的方案就是第三方依赖给我们提供一个注解,这个注解一般都是以@EnableXxxx开头,其中封装的就是@Import注解.
    1. 第三方提供注解
java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(TestConfig.class)
public @interface EnableTestConfig {
}

使用@Target标签定义注解在哪里标记,使用@Retention注解定义注解的生命周期.之后使用@Import注解导入第三方的类对象.

  1. 在启动类上提供第三方注解
java 复制代码
@SpringBootApplication
@EnableTestConfig
public class SpringPrincipleDemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(SpringPrincipleDemoApplication.class, args);
        TestConfig testConfig = context.getBean(TestConfig.class);
        System.out.println(testConfig);
    }
}

Bea对象依然可以拿到:

3.2 SpringBoot原理分析

3.2.1 源码

那么Spring究竟是如何实现自动导入的呢?接下来我们就来查看Spring的源码,我们从@SpringBootApplication 开始看起.

这个直接也是Spring实现自动配置的核心

java 复制代码
@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 {
....
}

@SpringBootAppliaction是一个组合注解,注解中包含了:

  1. 元注解:
    JDK中提供了4个标准的用来对注解类型进行注解的注解类,我们称之为meta-annotation(元注解),他们分别是:
    • @Target描述注解的使用范围(即被修饰的注解可以用在什么地方)
    • @Retention描述注解保留的时间范围
    • @Documented描述在使用javadoc工具为类生成帮助文档时是否要保留其注解信息(了解)
    • @Inherited使被它修饰的注解具有继承性(如果某个类使用了被@Inherited修饰的注解,其子类自动具有该注解)
  2. @SpringBootConfiguration:
java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

里面其实就是一个@Configuration,只不过就是封装了一层而已.

  1. @EnableAutoConfiguratio

这是Spring自动装配的核心机制,下面详细解释.

  1. @ComponentScan

excludeFilter是自定义过滤器,通常用于排除一些类,注解等.

3.2.2 @EnableAutoConfiguratio详解

我们来观察@EnableAutoConfiguration注解的实现:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	/**
	 * Environment property that can be used to override when auto-configuration is
	 * enabled.
	 */
	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	String[] excludeName() default {};

}

这个注解主要包含两部分:

  1. @Import(AutoConfigurationImportSelector.class)
    使用@Import注解,导入实现ImportSelector的类
java 复制代码
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered{
			@Override
			public String[] selectImports(AnnotationMetadata annotationMetadata) {
				if (!isEnabled(annotationMetadata)) {
					return NO_IMPORTS;
				}
				AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
				return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
			}
		}

selectImports方法有调用了getAutoConfigurationEntry方法,获取可以自动配置的配置类信息集合:

java 复制代码
	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);
	}

其中getCandidateConfigurations方法获取在配置文件中所有自动配置类的集合.

java 复制代码
	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;
	}

获取的是所有基于META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports配置文件和META-INF/spring.factories的配置文件.里面包含了很多第三方依赖的配置文件.

注意

  • 加载的时候并不是把所有的配置全部加载进来,他是根据不同的条件来导入对应的配置的,这和@Conditional注解有关系,这个注解是Spring底层的一个注解,就是根据不同的条件来进行自己不同条件的判断,如果满足指定的条件,配置才会生效.
  • META-INF/spring.factories文件是Spring内部提供的⼀个约定俗成的加载方式,只需要在模块的META-INF/spring.factories文件中配置即可, Spring就会把相应的实现类注入到Spring容器中.
  1. @AutoConfigurationPackage
    源码如下:
java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

}

这个注解主要是导入了配置文件AutoConfigurationPackages.Registrar.class

java 复制代码
	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容器中.其中PackageImports(metadata).getPackageNames().toArray(new String[0]))就是当前启动所在的包名.

所以:@AutoConfigurationPackage就是把启动类所在的包下面所有的组件全部都扫描注册操Spring容器中.

3.2.3 总结

SpringBoot自动装配原理大致如下:

当Spring项目启动的时候,就会自动把这些配置文件中的配置类通过@Import注解全部加载到SpringIoC容器中.

相关推荐
GJCTYU1 分钟前
spring中@Transactional注解和事务的实战理解附代码
数据库·spring boot·后端·spring·oracle·mybatis
艾迪的技术之路11 分钟前
redisson使用lock导致死锁问题
java·后端·面试
今天背单词了吗98029 分钟前
算法学习笔记:8.Bellman-Ford 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·开发语言·后端·算法·最短路径问题
天天摸鱼的java工程师32 分钟前
使用 Spring Boot 整合高德地图实现路线规划功能
java·后端
东阳马生架构1 小时前
订单初版—2.生单链路中的技术问题说明文档
java
咖啡啡不加糖1 小时前
暴力破解漏洞与命令执行漏洞
java·后端·web安全
风象南1 小时前
SpringBoot敏感配置项加密与解密实战
java·spring boot·后端
DKPT1 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
Percep_gan1 小时前
idea的使用小技巧,个人向
java·ide·intellij-idea