• 【原理篇】三、SpringBoot自动配置原理


    0、背景demo

    用一个循序渐进的示例来体验属性配置,方便后面理解自动配置,先准备几个demo类:

    @Data
    public class Cat{
    
    	private String name;
    	
    	private Integer age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    @Data
    public class Mouse{
    
    	private String name;
    	
    	private Integer age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    //猫和老鼠卡通类
    @Component
    public class CartoonCatAndMouse{
    
    	private Cat cat;
    
    	private Mouse mouse;
    
    	//提供个构造方法给两个属性赋值,不然默认null,下面会空指针
    	public CartoonCatAndMouse(){
    		cat = new Cat();
    		cat.setName("tom");
    		cat.setAge(3);
    		mouse= new Mouse();
    		mouse.setName("jerry");
    		mouse.setAge(3);
    	}
    
    	public void play(){
    		System.out.println(cat.getAge()+"岁的"+cat.getName()+"与"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    //启动类中调用下play方法
    @SpringBootApplication
    public class App{
    
    	public static void main(String[] args){
    		
    		ConfigurableApplicationContext ctx = SringApplication.run(app.class);
    		
    		CartoonCatAndMouse bean = ctx.getBean(CartoonCatAndMouse.class);
    		
    		bean.play();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    此时,配置都硬编码了,显然不合理。

    优化第一步:引入yaml配置并@ConfigurationProperties读取

    cartoon:
      cat:
        name: tom
        age: 3
      mouse:
        name: jerry
        age: 4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    @Component
    @ConfigurationProperties(prefix = "cartoon")
    //加个set,不然cat属性和mouse属性为null
    @Data  
    public class CartoonCatAndMouse{
    
    	private Cat cat;
    
    	private Mouse mouse;
    
    	public void play(){
    		System.out.println(cat.getAge()+"岁的"+cat.getName()+"与"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    此时,如果yaml配置中没有相关配置,则对应的对象为null,进而空指针。也就是说,一加@ConfigurationProperties(prefix = “cartoon”),我的类和yaml配置绑死了,没配置,类都受影响。

    优化第二步:引入独立配置类CartoonProperties

    @ConfigurationProperties(prefix = "cartoon")
    @Data
    //旧知识点,读取yaml的类的对象必须受Spring容器管控,否则,即使拿到yaml值也无法set给你
    @Component
    public class CartoonProperties {
    	private Cat cat;
    	private Mouse mouse;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    @Component
    @Data  
    public class CartoonCatAndMouse{
    
    	private Cat cat;
    
    	private Mouse mouse;
    	
    	//构造器注入了,用@Autowired也行
    	private CartoonProperties cartoonProperties;
    
    	public void play(CartoonProperties cartoonProperties){
    		
    		this.cartoonProperties = cartoonProperties;
    		cat = new Cat();
    		cat.setName(cartoonProperties.getCat()!=null && StringUtils.hasText(cartoonProperties.getCat().getName())
    					?cartoonProperties.getCat().getName():"tom");  //有则用,无则用默认值
    		//Mouse对象同样写法,略
    		System.out.println(cat.getAge()+"岁的"+cat.getName()+"与"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    此时,有个缺点,CartoonProperties类不管用不用,都被强制加载成一个Bean了,但去掉@Component,只留@ConfigurationProperties语法错误 ⇒ 用@EnableConfigurationProperties

    优化第三步:@EnableConfigurationProperties,改掉强制加载Bean

    //@Component 不再需要定义为Bean
    @ConfigurationProperties(prefix = "cartoon")
    @Data
    public class CartoonProperties {
    	private Cat cat;
    	private Mouse mouse;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    @ConfigurationProperties和@EnableConfigurationProperties,前者是做属性绑定的,后者是开启属性绑定,并设定对应的目标是谁

    @Component  //这个Component也可以去掉,用的时候@Import(CartoonCatAndMouse.class)
    @Data  
    //即当我加载CartoonCatAndMouse时,就用CartoonProperties.class,并把CartoonProperties类加载成Bean
    @EnableConfigurationPropertiesCartoonProperties.class)   
    public class CartoonCatAndMouse{
    
    	private Cat cat;
    
    	private Mouse mouse;
    
    	//重复代码,略....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这套模式的亮点有以下几个:

    • 合理的加载配置文件,即你配置了就用你的,没配就用默认值来工作
    • 对于属性类xxxProperties不用强制配置成Bean,使用@EnableConfigurationProperties
    • 对于业务功能的Bean,通常使用@Import将一个类加载成Bean,来解耦强制加载Bean,以降低Spring容器管理Bean的工作量以及强度

    1、自动配置思路

    • 收集Spring开发者的编程习惯,得到常用技术集列表 ⇒ 技术集A
    • 收集每个技术的常用参数值,得到常用配置值列表 ⇒ 设置集B
    • 初始化你项目的SpringBoot基础环境,包括加载用户自定义的Bean以及用户导入的其他坐标,得到初始化环境
    • 把技术集A中具有使用条件的技术设置为按条件加载,与初始化环境对比,如有Redis核心类时,就触发加载Redis技术集的资源
    • 约定大于配置,设置集B里的参数值做为默认配置加载
    • 对于要修改的配置,开发者自行覆盖

    总之就是,SpringBoot官方整理了常用的技术以及对应的配置值,可根据你项目的环境来自动加载相关的Bean,并给你默认配置,减少开发者的工作量。

    2、META-INF/spring.factories

    从启动类的@SpringBootApplication注解开始看:

    在这里插入图片描述
    关键点:

    • @Import(AutoConfigurationPackages.Registrar.class) ,debug可发现,这里是设置当前配置所在的包为扫描包,后续要针对当前包进行扫描
    • @Import(AutoConfigurationImportSelector.class)

    继续往下看AutoConfigurationImportSelector,它实现了很多接口,可分三类:

    • DeferredImportSelector文末补充点一节
    • Ordered:有关Bean的创建顺序

    重写的DeferredImportSelector接口中的process方法中除去断言代码,往下debug看getAutoConfigurationEntry方法:

    在这里插入图片描述

    调用了获取候选配置的方法getCandidateConfigurations:

    在这里插入图片描述

    跳过断言代码,看到核心肯定在loadFactoryNames方法:

    在这里插入图片描述
    继续debug方法loadFactoryNames,除去判空,关键的在loadSpringFactories方法:

    在这里插入图片描述

    可以看到loadSpringFactories方法在读META-INF/spring.factories里的资源,这里就是核心了。

    在这里插入图片描述

    3、Redis自动配置

    查看SpringBootAutoConfiguration下的spring.factories文件:

    在这里插入图片描述

    以Redis的自动装配为例来看:spring.factories文件钟找到了RedisAutoConfiguration类,即Redis的自动配置类:

    在这里插入图片描述

    对比前面刚开始的背景案例,这个Redis的自动装配就很明晰了:

    • @ConditionOnClass是Redis相关Bean加载的条件,引入Redis起步依赖后,里面包含这里的RedisOperations.class类,也就满足了加载条件
    • @EnableConfigurationProperties后面是属性配置类
    • @Import导入两个Redis底层的Bean
    • 定义两个Redis客户端操作的Template的Bean,并前提是用户没有自定义这个Bean

    这就是前面提到的,SpringBoot加了无数技术的自动配置类,用对应的条件来检测你当前Spring项目中要不要加对应的技术的Bean等资源。

    4、自定义一个自动配置

    对比上面的Redis自动配置类和开篇的demo,前面的CartoonCatAndMouse类就是RedisAutoConfiguration,CartoonProperties就是RedisProperties,那就模仿官方写法,把CartoonCatAndMouse改成一个真正的自动配置类。新建META-INF/spring.factories,内容:

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.plat.bean.CartoonCatAndMouse
    
    • 1
    • 2
    • 3

    此时,CartooCatAndMouse就完成了自动配置,修一下,加上加载条件:

    @Data 
    @ConditionalOnClass(name = "com.plat.core.MyCore.class")
    @EnableConfigurationPropertiesCartoonProperties.class)   
    public class CartoonCatAndMouse{
    
    	private Cat cat;
    
    	private Mouse mouse;
    
    	//重复代码,略....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此时,项目启动,有MyCore.class类被加载时,触发自动装配,完成相关Bean的加载。最后,如果直接把一个类配置到spring.factories文件中,能从IoC容器中get到这个Bean吗? ==> 可以,但属性都为null

    5、排除SpringBoot内置自动配置类的加载

    截至SpringBoot2.6版本,spring.factories中已有130多种技术对应的自动配置类,没必要启动时去全部判断一次,可以直接排除掉一些你肯定不用的自动配置类。

    • 方法一:配置文件
    spring:
      autoconfigure:
        exclude: 
          - org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
          - org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 方法二:注解
    @SpringBootApplication(excludeName = "",exclude = {})
    
    //exclude属性其实是@EnableAutoConfiguration注解的,但它被@SpringBootApplication包含,属性也被拿了过来
    
    • 1
    • 2
    • 3
    • 方式三:排除掉依赖,直接通过干涉激活条件@Conditional实现,如去除tomcat自动配置(条件激活),添加jetty自动配置(条件激活)

    在这里插入图片描述

    6、补充点:ApplicationContextAware接口

    获取ApplicationContext对象,可以通过实现ApplicationContextAware接口:

    @Component
    public class MyIocUtils implements ApplicationContextAware {
    
        private static ApplicationContext applicationContext;
        
        @Override
        public void setApplicationContext(ApplicationContext applicationContext){
            this.applicationContext = applicationContext;    //给当前工具类的applicationContext属性赋值
        }
     
        public static ApplicationContext getApplicationContext(){
            return applicationContext;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此时,在你需要使用上下文对象的地方直接:

    MyIocUtils.getApplicationContext()
    
    • 1

    即可拿到上下文对象。当然你不封装成工具类也行,直接在你需要用上下文对象的类里写:

    public class YourClass{
    
    	private ApplicationContext applicationContext;
    	
    	@Override
        public void setApplicationContext(ApplicationContext applicationContext){
            this.applicationContext = applicationContext;    //给当前类的applicationContext属性赋值
        }
    
    	public void doSome(){
    		//使用上下文对象
    		applicationContext.getBean........
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • 相关阅读:
    Github 2024-02-13 开源项目日报 Top9
    PostgreSQL 介绍
    上海自动驾驶再上热搜,为什么是它?丨曼孚科技
    接口测试--知识问答
    我的DW个人网站设计——安徽宣城6页HTML+CSS+JavaScript
    同花顺_代码解析_技术指标_L
    Web应用安全测试-防护功能缺失
    leetCode-hot100-数组专题之求和+数学定理+其他
    docker安装nacos和sentinel笔记
    shell编程
  • 原文地址:https://blog.csdn.net/llg___/article/details/134272902