SpringBoot自动配置的原理及实现「终于解决」

SpringBoot自动配置的原理及实现「终于解决」SpringBoot自动配置的实现原理SpringBoot的核心就是自动配置,自动配置又是基于条件判断来配置Bean。关于自动配置的源码在spring-boot-autoconfigure-2.0.3.RELEASE.jar回顾配置属性在通常需要我们在property中配置信息时,通常使用@ConfigurationProperties(pefix=“前缀”)注解的方式从配置文件中获取配置…………

大家好,欢迎来到IT知识分享网。

整理日志

2018-11-13 写blog
2020-11-13 参考其他blog添加流程图
2021-3-17 评论区说跨域的包没引入,这是我当时写脚手架时加的,对于当前blog没什么用,我加上了注释 //,免得被喷
2022-07-12 新增SpringFactoriesLoader,springboot应用启动时ConfigurationClassPostProcessor的注册

用过 Spring Boot 的都知道

@ComponentScan 注解的作用是扫描 @SpringBootApplication 所在的 Application 类所在的包(basepackage)下所有的 @component 注解(或拓展了 @component 的注解)标记的 bean,并注册到 spring 容器中。

那么问题来了

在 Spring Boot 项目中,如果你想要被 Spring 容器管理的 bean 不在 Spring Boot 包扫描路径下,怎么办?
解决 Spring Boot 中不能被默认路径扫描的配置类的方式,有 2 种:

(1)在 Spring Boot 主类上使用 @Import 注解
(2)使用 spring.factories 文件自动配置

SpringBoot自动配置的实现原理

SpringBoot的核心就是自动配置,自动配置又是基于条件判断来配置Bean。关于自动配置的源码在spring-boot-autoconfigure-2.0.3.RELEASE.jar

在这里插入图片描述

回顾配置属性

在通常需要我们在property中配置信息时,通常使用@ConfigurationProperties(pefix=“前缀”)注解的方式从配置文件中获取配置,如下:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.boot.context.properties.ConfigurationProperties;

@RestController
@ConfigurationProperties(prefix = "test")
//@Component //如果这里添加了注解那么在自动配置类的时候就不用添加@enableConfigurationProperties(HelloProperties.class)注解.
public class Demo {

 
	
    private String msg="default";//现在我们在配置文件写hello.msg=world,因为简单就不再展示;如果那么默认为default.

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
	}
	
	
	@RequestMapping("/msg")
	public Object index(){
		return this.msg;
	}

}

application.yml中配置信息

test:
  msg: bamboo

访问url获取配置信息返回的值
http://localhost:8080/msg

如果把application.yml中的配置信息注释掉则默认使用default值,否则使用配置信息中的值,以上便是普通配置方式

自动配置流程解析和关键类SpringFactoriesLoader

SpringBoot运行原理
先看@SpringBootApplication

@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而省略@EnableAutoConfiguration
注解@SpringBootApplication隐含了spring-boot-autoconfigure的注解@EnableAutoConfiguration

主要关注的几个注解如下
@SpringBootConfiguration:标记当前类为配置类
@EnableAutoConfiguration:开启自动配置
@ComponentScan:扫描主类所在的同级包以及下级包里的Bean
关键是@EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //默认将主配置类( @SpringBootApplication )所在的包及其子包里面的所有组件扫描到IOC容器中。
@Import(AutoConfigurationImportSelector.class) //加载所有符合条件的META-INF/spring.factorie类
public @interface EnableAutoConfiguration { 
   
   String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
   Class<?>[] exclude() default { 
   };
   String[] excludeName() default { 
   };
}

AutoConfigurationPackages将主配置类( @SpringBootApplication )所在的包及其子包里面的所有组件扫描到IOC容器中

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage { 
   
}


/**AutoConfigurationPackages 的静态内部类: Registrar * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing * configuration. */
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));
    }

}

AutoConfigurationImportSelector 加载所有符合条件的META-INF/spring.factorie类

AutoConfigurationImportSelector 接口何时被执行?

SpringBoot 启动时会使用 ConfigurationClassParser 来解析被 @Configuration 标识的配置类, 然后再处理这个类内部被其他注解修饰的情况, 比如 @Import 注解, @ComponentScan 注解,@Bean 注解等

如果发现注解中存在 @Import(ImportSelector) 的情况下,就会创建一个相应的 ImportSelector 对象,并调用其 process 方法

@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { 
   
    Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
                 () -> String.format("Only %s implementations are supported, got %s",
                                     AutoConfigurationImportSelector.class.getSimpleName(),
                                     deferredImportSelector.getClass().getName()));
                                     //实现真正逻辑的是在 调用的 getAutoConfigurationEntry 方法
    AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
        .getAutoConfigurationEntry(annotationMetadata);
    this.autoConfigurationEntries.add(autoConfigurationEntry);
    for (String importClassName : autoConfigurationEntry.getConfigurations()) { 
   
        this.entries.putIfAbsent(importClassName, annotationMetadata);
    }
}


protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { 
   
    if (!isEnabled(annotationMetadata)) { 
   
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    
    // 获取 xxxConfiguration 类
    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得到待配置的class的类名集合,这个集合就是所有需要进行自动配置的类名,而是是否配置的关键在于META-INF/spring.factories文件中是否存在该配置信息

  protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) { 
   
            //返回META-INF/spring.factorie里面的所有类名列表
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
                getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

spring-core 包里定义了 SpringFactoriesLoader 类,这个类实现了检索 META-INF/spring.factories 文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:

loadFactoryNames根据接口获取其接口类的名称,这个方法返回的是类名的列表-上面用的就是这个方法
loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。

loadFactoryNames这个方法中会遍历整个 spring-boot 项目的 classpath 下 ClassLoader 中所有 jar 包下的 spring.factories文件。也就是说我们可以在自己的 jar 中配置 spring.factories 文件,不会影响到其它地方的配置,也不会被别人的配置覆盖 。

打开,如下图可以看到所有需要配置的类全路径都在文件中,每行一个配置,多个类名逗号分隔,而\表示忽略换行
在这里插入图片描述
可以看出 spring.factories 文件可以将 spring-boot 项目包以外的 bean(即在 pom 文件中添加依赖中的 bean)注册到 spring-boot 项目的 spring 容器。由于@ComponentScan 注解只能扫描 spring-boot 项目包内的 bean 并注册到 spring 容器中,因此需要 @EnableAutoConfiguration 注解来注册项目包外的bean。而 spring.factories 文件,则是用来记录项目包外需要注册的bean类名

应用启动时ConfigurationClassPostProcessor的注册

Springboot 应用在启动时,会创建一个ApplicationContext上下文实例。如果是Web应用,该上下文实例会使用类 AnnotationConfigEmbeddedWebApplicationContext来创建;否则使用类AnnotationConfigApplicationContext来创建。

不管是以上两种情况中的哪一种,相应的上下文类实例在创建时都会创建一个AnnotatedBeanDefinitionReader实例用于读取注解方式的Bean定义,而在创建这个AnnotatedBeanDefinitionReader实例的过程中,它注册了一个 ConfigurationClassPostProcessor Bean定义到容器,这是一个BeanFactoryPostProcessor,更明确地讲,它是一个BeanDefinitionRegistryPostProcessor。

ConfigurationClassPostProcessor被设计用来发现所有的配置类和相关的Bean定义并注册到容器,它在所有BeanFactoryPostProcessor中具备最高执行优先级,因为其他BeanFactoryPostProcessor需要基于注册了Bean定义工作

在springboot启动过程中,具体的来讲,是在以下位置被完成自动配置

SpringApplication.run()
=> refreshContext()
	=> AbstractApplicationContext.refresh()
	=> invokeBeanFactoryPostProcessors()
		=> ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()
		=> processConfigBeanDefinitions()
			=> ConfigurationClassParser.parse()
			=> processDeferredImportSelectors()
				=> AutoConfigurationImportSelector.selectImports()

pring-boot-autoconfigure.jar包内文件的结构。

spring-boot-autoconfigure jar包的文件结构
├─META-INF
│      spring-autoconfigure-metadata.properties
│      spring.factories
│
└─org
    └─springframework
        └─boot
            └─autoconfigure

在启动时把实现类org.springframework.boot.autoconfigure.EnableAutoConfiguration定义了哪些类是自动配置类,使用属性org.springframework.boot.autoconfigure.AutoConfigurationImportFilter定义了哪些自动配置导入过滤器会被应用,缺省情况下,只有一个过滤器会被应用,那就是OnClassCondition

AutoConfigurationImportSelector.selectImports()在执行时 :

1.从spring-autoconfigure-metadata.properties属性文件中提取自动配置元数据AutoConfigurationMetadata;

2.获取spring.factories属性文件中属性org.springframework.boot.autoconfigure.EnableAutoConfiguration定义的所有配置类作为候选配置类;

3.然后,获取spring.factories属性文件中属性org.springframework.boot.autoconfigure.AutoConfigurationImportFilter定义的过滤器,缺省是一个 : org.springframework.boot.autoconfigure.condition.OnClassCondition

4.对所有的候选配置类在自动配置元数据上应用所发现的过滤器,其实缺省情况下就一个,就是 OnClassCondition,也就是利用每个候选配置类上的注解 @ConditionalOnClass , 来看这个候选配置类是否需要被应用。

selectImports()会返回所有需要被应用的配置类(注解@ConditionalOnClass条件被满足),ConfigurationClassPostProcessor.postProcessBeanFactory()会将识别这些配置类中定义的bean并将它们注册到容器

在这里插入图片描述
整个流程如上图所示

样例讲解

以SpringApplicationAdminJmxAutoConfiguration类来看其主要构成部分

@Configuration
@AutoConfigureAfter({JmxAutoConfiguration.class}) //配置完JmxAutoConfiguration后再配置当前类型
// spring.application.admin为前缀,属性为enabled,有值时为true,没有匹配到则为false:以上条件为true则实例化,否则不是实例化
@ConditionalOnProperty( prefix = "spring.application.admin", value = {"enabled"}, havingValue = "true",  matchIfMissing = false)
public class SpringApplicationAdminJmxAutoConfiguration 

都能看到各种各样的条件判断注解,满足条件时就加载这个Bean并实例化
此类的条件注解是:@ConditionalOnProperty

@ConditionalOnBean:当容器里有指定Bean的条件下
@ConditionalOnClass:当类路径下有指定的类的条件下
@ConditionalOnExpression:基于SpEL表达式为true的时候作为判断条件才去实例化
@ConditionalOnJava:基于JVM版本作为判断条件
@ConditionalOnJndi:在JNDI存在的条件下查找指定的位置
@ConditionalOnMissingBean:当容器里没有指定Bean的情况下
@ConditionalOnMissingClass:当容器里没有指定类的情况下
@ConditionalOnWebApplication:当前项目时Web项目的条件下
@ConditionalOnNotWebApplication:当前项目不是Web项目的条件下
@ConditionalOnProperty:指定的属性是否有指定的值
@ConditionalOnResource:类路径是否有指定的值
@ConditionalOnOnSingleCandidate:当指定Bean在容器中只有一个,或者有多个但是指定首选的Bean
这些注解都组合了@Conditional注解,只是使用了不同的条件组合最后为true时才会去实例化需要实例化的类,否则忽略
这种spring4.X带来的动态组合很容易后期配置,从而避免了硬编码,使配置信息更加灵活多变,同时也避免了不必要的意外异常报错。使用的人只要知道配置的条件即可也不用去阅读源码,方便快捷,这也是sprignboot快捷方式带来的好处

参考HttpEncodingAutoConfiguration配置信息如下

@Configuration
@EnableConfigurationProperties(HttpEncodingProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {

@Configuration:标明为配置类
@EnableConfigurationProperties(HttpEncodingProperties.class)声明开启属性注入
@ConditionalOnClass(CharacterEncodingFilter.class)当CharacterEncodingFilter在类路径的条件下
@ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true)当spring.http.encoding=enabled的情况下,如果没有设置则默认为true,即条件符合
@ConditionalOnMissingBean当容器中没有这个Bean时新建Bean

案例扩展


/**
 * @author wuweifeng wrote on 2017/11/25.
 * 根据部署环境动态决定是否启用eureka
 线上的环境开启eureka,就在application-prod.yml里配上open.eureka=true,
 其他的yml什么也不写就行了。这样本地启动时就相当于没有开启EnableDiscoveryClient
 */
@Component
@ConditionalOnProperty(value = "open.eureka")
@EnableDiscoveryClient
public class JudgeEnableDiscoveryClient 

自己实现一个自己的自动配置

项目

xm-common:普通jar项目
- src/main
    java
        BambooServer.java  需要被实例化的服务类
        BambooServerProperties.java 配置信息属性类
        BmbooServiceAutoConfiguration.java 自动配置类
    resources
        META-INF/spring.factories 配置自动配置的属性文件
demo:普通springboot-web项目

需要实例化的服务类

public class BambooServer {
    private String name;

    public String sayServerName(){
        return "I'm " + name + "! ";
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

配置信息对应的属性映射类,需要pom中加入spring-boot-starter依赖

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "bamboo")
public class BambooServerProperties {

    private static final String NAME = "bamboo_server0";

    private String name = NAME;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

自动配置文件

/**
 * Author: bamboo
 * Time: 2018/11/25/025
 * Describe: 自动配置类
 * 根据条件判断是否要自动配置,创建Bean
 */
@Configuration
@EnableConfigurationProperties(BambooServerProperties.class)//声明开启属性注入BambooServerProperties
@ConditionalOnClass(BambooServer.class)//判断BambooServer这个类在类路径中是否存在,只有存在时才符合条件
@ConditionalOnProperty(prefix = "bamboo",value = "enabled",matchIfMissing = true)
public class BmbooServiceAutoConfiguration {

    @Autowired
    private BambooServerProperties mistraServiceProperties;

    @Bean(name = "bambooServer")
    @ConditionalOnMissingBean(BambooServer.class)//当容器中没有这个Bean时(BambooServer)就自动配置这个Bean,Bean的参数来自于BambooServerProperties
    public BambooServer mistraService(){
        BambooServer mistraService = new BambooServer();
        mistraService.setName(mistraServiceProperties.getName());
        return mistraService;
    }
}

在创建如下路径文件src/main/resources/META-INF/spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.bamboo.common.autoconfigure.bamboo.BmbooServiceAutoConfiguration

必须是自动配置类的全路径

mvn install 该项目

创建一个springboot-mvc项目pom依赖上面的jar

@SpringBootApplication
@RestController
//@Import(value = {CorsConfig.class, LogFilter.class}) //跨域,接口访问请求日志
public class DemoApplication {
	@Autowired
	private BambooServer bmbooService;

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@RequestMapping("/")
	public Object index(){
		return "helll demo"+bmbooService.getName()+DateUtils.getDate();
	}
}

http://localhost:8080/则返回当前服务的默认值

在applicaton.yml中加,重启刷新则会更新为如下信息

bamboo:
  name: 测试服务

总结图

在这里插入图片描述
SpringBoot自动化配置关键组件关系图
mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。

参考blog

https://www.cnblogs.com/xiaoxi/p/7999885.html
https://blog.csdn.net/andy_zhang2007/article/details/78582465

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/24772.html

(0)
上一篇 2023-08-09 20:00
下一篇 2023-08-14 14:00

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信