面试的时候,其实远远不需要回答的这么具体,你只需要这样回答:
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
一、前言
相信接触过springboot的朋友都知道,springboot有各种Starter方便引入依赖,同时通过IDE想要什么依赖直接勾选加进来就可以了,非常方便。SpringBoot的核心就是自动配置,而支持自动配置的是一个个Starter项目。除了官方已有的starter,用户自己也可以根据规则自定义自己的Starter项目。
而Starter与自动配置的关系个人理解是这样的:项目在添加某一个Starter后,如果没有特殊需要,基本上是不需要任何配置可以直接使用的,原因是这个Starter已经通过【自动配置】加载了一些默认配置;如果有需要,可以通过springboot配置文件(比如application.yml)设置一些配置项覆盖默认配置。
二、自定义Starter
1、说明
个人认为,理解【自动配置】的原理之前,最好先了解自定义starter的过程,这样更方便理解自动配置。
自定义starter场景:如果springboot自带的入口类不能满足要求,则可以自定义Starter
2、使用
1)创建项目
首先要为starter起名字:
- Spring官方的Starter:命名为【spring-boot-starter-名字】,如:spring-boot-starter-web
- Spring官方建议非官方的自定义Starter:命名为【名字-spring-boot-starter】,如myxxx-spring-boot-starter
使用IDEA创建一个普通的maven项目
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
说明1:其中spring-boot-starter-web里面会引用spring-boot-starter里面会引用spring-boot-autoconfigure(网上有很多示例是直接引用后面两个其中之一的,也是可以的)
说明2:为了使用dependencyManagement进行依赖的版本管理,可以设置自定义starter项目的父项目为spring-boot-starter-parent
说明3:自定义的Starter时不能有启动入口的,也就是说它只能作为工具类项目。所以,不要把自定义Starter的pom.xml写成一个可启动的项目
完整pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mzj.myproject</groupId>
<artifactId>myproject-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<name>myproject-spring-boot-starter</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3)自定义properties类
说明:在使用Spring官方提供的Starter时,可以在application.yml或者application.properties文件中配置参数,以覆盖默认值。在自定义Starter时,也可以根据需要来配置properties类,以保存配置信息。这里配置前缀统一采用spring.mystarter
package com.mzj.myproject;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.mystarter")
public class MyStarterProperties {
// 参数
private String parameter;
private String url;
private int port;
public String getParameter() {
return parameter;
}
public void setParameter(String parameter) {
this.parameter = parameter;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
4)定义核心服务类
说明:每个starter都有自己要处理的业务功能,因此定义服务类,这个服务类有两个作用,一个为引入的项目本身的功能性服务,另外一个用来springboot自动配置时的判断依据。
package com.mzj.myproject;
public class MyStarterService {
private MyStarterProperties myproperties;
public MyStarterService() {
}
public MyStarterService(MyStarterProperties myproperties) {
this.myproperties = myproperties;
}
public String print() {
System.out.println("参数1: " + myproperties.getParameter());
System.out.println("参数2: " + myproperties.getUrl());
System.out.println("参数3: " + myproperties.getPort());
String s = myproperties.getParameter();
return s;
}
}
5)定义自动配置类
每个Starter一般至少有一个自动配置类,命名规则为:【名字+AutoConfiguration】,如:MyStarterServiceAutoConfiguration。
@Configuration用来声明该类为一个配置类;
@ConditionalOnClass注解说明只有当MyStarterService类存在于classpath中时才会进行自动配置;
@EnableConfigurationProperties作用时使使用 @ConfigurationProperties 注解的类生效,这里是MyStarterProperties,也就是将application.properties中对应的属性配置设置于MyStarterProperties对象中;
myStarterService方法上的注解,
- @Bean表明该方法实例化的对象会被加载到容器当中;
- @ConditionalOnMissingBean当容器中没有指定Bean的情况下,自动配置MyStarterService类
- @ConditionalOnProperty指定了配置文件中spring.mystarter.enabled=true时才进行相应的自动装配。
配置方法见以下代码:
package com.mzj.myproject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(MyStarterProperties.class)//@EnableConfigurationProperties注解的作用是:使使用 @ConfigurationProperties 注解的类生效,这里是MyStarterProperties
/**
* Description: 当类路径classpath下有指定的类的情况下进行自动配置
*/
@ConditionalOnClass(MyStarterService.class)
/**
* Description: 配置文件中matchIfMissing =true时进行自动配置
*
* 配置文件中spring.mystarter.enabled=true时才进行相应的自动装配
*/
@ConditionalOnProperty(prefix = "spring.mystarter", value = "enabled", matchIfMissing = true)
public class MyStarterServiceAutoConfiguration {
@Autowired
private MyStarterProperties myproperties;//使用配置
@Bean
/**
* Description: 当容器中没有指定Bean的情况下,自动配置MyStarterService类
*/
@ConditionalOnMissingBean(MyStarterService.class)
public MyStarterService myStarterService() {
MyStarterService myStarterService = new MyStarterService(myproperties);
return myStarterService;
}
}
其中MyStarterService的创建是在Configuration类(MyStarterServiceAutoConfiguration)中的myStarterService方法创建的
最后,当所有的基础代码和自动配置类都准备完成,就需要对其进行注册:在resources文件下新建目录META-INF,在目录中新建spring.factories文件,并在文件中配置自动配置类XXXXAutoConfiguration(MyStarterServiceAutoConfiguration):
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mzj.myproject.MyStarterServiceAutoConfiguration
在spring.factories配置文件中注册MyStarterServiceAutoConfiguration类。如果有多个自动配置类,用逗号分隔换行即可。
6)打包发布
至此,一个基于Spring Boot的自动配置starter便完成了。
在完成上面的配置后,就可以打包生成JAR文件,然后就可以像使用官方Starter那样使用了。使用“maven:install”将其打包到本地maven仓库或上传至私服。其他项目便可以通过maven依赖使用。
如果不发布到Maven仓库,则需要用户手动添加依赖:
7)测试
新建普通springboot项目,IDEA创建项目时,只选择了依赖spring-boot-starter-web
之后在pom.xml中增加我们自定义starter依赖:
<dependency>
<groupId>com.mzj.myproject</groupId>
<artifactId>myproject-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
测试一:
在项目application.yml中设置加入以下参数:
然后需要使用的地方注入starter包中的service依赖即可,比如下面的Controller中:
package com.mzj.myproject.controller;
import com.mzj.myproject.MyStarterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("teststarter")
public class TestController {
@Autowired
private MyStarterService myStarterService;
@RequestMapping(value = "get", method = RequestMethod.GET)
public String teststarter(String userName) {
System.out.println("myStarterService :" + myStarterService.print());
return userName;
}
}
然后启动springboot项目,postman输入:http://localhost:8080/teststarter/get?userName=123
控制台输出:
测试二:把配置文件中spring.mystarter.enabled=false,则不进行自动装配,访问服务时出现未找到异常:
如果不设置spring.mystarter.enabled属性,默认为true
8)结论
通过此示例可以看出,MyStarterService对象被自动配置,并且测试通过。
而针对我们自定义的starter,可以进行进一步拓展,实现各种基础功能,而当其他项目需要时只需【引入对应的依赖】+【配置具体的参数】即可马上使用,是不是非常方便?
9)总结
正规的Starter是一个独立的工程,可以在Maven仓库中注册、发布,以便供其他开发人员使用,自定义Starter包括以下几方面内容:
- 自动配置文件:根据classpath下是否存在指定的类来决定是否要执行该功能的自动配置(存在才进行自动配置——自动配置类的@ConditionalOnClass(MyStarterService.class)注解)
- 可以通过@ConditionalOnProperty注解设置自动装配是否启动可以通过参数进行设置
- spring.factories:指导springboot找到指定的自动配置文件
- endpoint:包含对服务的描述、界面、交互(业务信息的查询)——本文未涉及
- health indicator:该Starter提供服务的健康指标——本文未涉及
三、springboot自动配置原理
Spring Boot关于自动配置的源码在spring-boot-autoconfigure-x.x.x.x.jar中。
掌握了自定义starter,接下来看看spring自动配置的原理:
一、springboot程序入口类要使用@SpringBootApplication注解声明,它是SpringBoot的核心注解
package com.mzj.myproject;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
二、@SpringBootApplication注解里面,最主要的就是@EnableAutoConfiguration注解,这么直白的名字,一看就知道它要开启自动配置,再进去@EnableAutoConfiguration注解。
三、@EnableAutoConfiguration注解也是一个派生注解,可以看到,在@EnableAutoConfiguration注解内使用到了@import注解来完成导入配置的功能:
package org.springframework.boot.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.support.SpringFactoriesLoader;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
//略
}
而@Import注解的参数AutoConfigurationImportSelector内部则是使用了SpringFactoriesLoader.loadFactoryNames方法进行扫描具有META-INF/spring.factories文件的jar包。下面是2.1.16.RELEASE实现源码:
这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(...)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
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 = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
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;
}
任何一个springboot应用,都会引入spring-boot-autoconfigure(因为springboot入口类需要通过@SpringBootApplication注解修饰,而这个注解就是在spring-boot-autoconfigure包,具体方式是通过直接引入或者通过引入比如spring-boot-starter-web间接引入),而spring.factories文件就在该包下面。spring.factories文件是Key=Value形式,多个Value时使用,隔开,该文件中定义了:
- 初始化信息
- 监听器信息(应用监听器、自动配置导入监听器)
- 过滤信息
- 需要springboot进行自动配置的类范围的key:org.springframework.boot.autoconfigure.EnableAutoConfiguration,如下所示:
上面的EnableAutoConfiguration配置了多个类,这些都是Spring Boot中内置的需要进行自动配置相关类;在启动过程中会解析对应类配置信息。每个XXXConfiguation类都定义了相关bean的实例化配置。都说明了哪些bean可以被自动配置,什么条件下可以自动配置,并把这些bean实例化出来。
如果我们自定义了一个starter的话,也要在该starter的jar包中提供 spring.factories文件,并且为其配置org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的配置类。所有框架的自动配置流程基本都是一样的:
- 判断是否引入框架(使用各种条件注解限定自动配置生效条件)
- 获取配置参数
- 根据配置参数初始化框架相应组件
每一个XxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在Spring Boot中以注解的形式体现,以下为SpringBoot内置条件注解(红色的是经常使用的):
- @ConditionalOnBean:当SpringIoc容器内存在指定Bean的条件
- @ConditionalOnClass:当SpringIoc容器内存在指定Class的条件
- @ConditionalOnExpression:基于SpEL表达式作为判断条件
- @ConditionalOnJava:基于JVM版本作为判断条件
- @ConditionalOnJndi:在JNDI存在时查找指定的位置
- @ConditionalOnMissingBean:当SpringIoc容器内不存在指定Bean的条件
- @ConditionalOnMissingClass:当SpringIoc容器内不存在指定Class的条件
- @ConditionalOnNotWebApplication:当前项目不是Web项目的条件
- @ConditionalOnProperty:指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。
- @ConditionalOnResource:类路径是否有指定的值
- @ConditionalOnSingleCandidate:当指定Bean在SpringIoc容器内只有一个,或者虽然有多个但是指定首选的Bean
- @ConditionalOnWebApplication:当前项目是Web项目的条件
以上注解都是元注解@Conditional演变而来的,根据不用的条件对应创建以上的具体条件注解。
举例讲解
本小节参考:https://blog.csdn.net/u014745069/article/details/83820511
接下来,以ServletWebServerFactoryAutoConfiguration配置类为例,解释一下全局配置文件中的属性如何生效,比如:server.port=8081,是如何生效的(当然不配置也会有默认值,这个默认值来自于org.apache.catalina.startup.Tomcat)。
上图中第2个红框用来加载我们自定义的配置(比如:server.port=9090),下面的红框加载默认配置(以tomcat配置作为默认配置),这种设置默认配置的方式适用于涉及多个配置中有一些需要自定义,有一些采用默认配置,比如server.ip进行自定义,而server.port采用默认的
在ServletWebServerFactoryAutoConfiguration类上,有一个@EnableConfigurationProperties注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。
在这个类上,我们看到了一个非常熟悉的注解:@ConfigurationProperties,它的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责导入这个已经绑定了属性的bean到spring容器中(见上面截图)。那么所有其他的和这个类相关的属性都可以在全局配置文件中定义,也就是说,真正“限制”我们可以在全局配置文件中配置哪些属性的类就是这些XxxxProperties类,它与配置文件中定义的prefix关键字开头的一组属性是唯一对应的。
至此,我们大致可以了解。在全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。
而诸多的XxxxAutoConfiguration自动配置类,就是Spring容器的JavaConfig形式,作用就是为Spring 容器导入bean,而所有导入的bean所需要的属性都通过xxxxProperties的bean来获得。
面试的时候,其实远远不需要回答的这么具体,你只需要这样回答:
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
通过一张图标来理解一下这一繁复的流程:
springboot自动化配置默认值设置与自动配置覆盖默认值(20201012)
1、ServletWebServerFactoryConfiguration:
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
------创建TomcatServletWebServerFactory中默认属性为8080
2、WebServerFactoryCustomizerBeanPostProcessor:
执行流程
3、ServletWebServerFactoryAutoConfiguration
加载spring.factories文件中Configuration类,创建Service类(通过Properties创建,这里完成了自动配置8081注入创建的Service对象)return new TomcatServletWebServerFactoryCustomizer(serverProperties);
这里Service类是TomcatServletWebServerFactoryCustomizer
4、执行Service的方法,这里是ServletWebServerFactoryCustomizer的customize方法:将个性化值覆盖默认值
总结
综上是对自动配置原理的讲解。当然,在浏览源码的时候一定要记得不要太过拘泥与代码的实现,而是应该抓住重点脉络:
- 一定要记得XxxxProperties类的含义是:封装配置文件中相关属性;
- XxxxAutoConfiguration类的含义是:自动配置类,目的是给容器中添加组件。
- 而其他的主方法启动,则是为了加载这些五花八门的XxxxAutoConfiguration类。
附录
1、springboot支持在application.yml中配置的参数