文章目录
前言
springboot一直在使用,省去了手动添加很多配置,非常方便;今天总结下对springboot自动配置的一些理解(基于springboot2.1.4,注意2.X和1.X区别较大)。
springboot项目,实际上是一段代码,带有main方法入口的代码,甚至内嵌了tomcat容器的全部代码;
新创建的springboot项目,都会默认生成一个Application.java类(没有生成的,自己去手动创建),如下图:
打开这个类:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
发现只有一个main方法和一个@SpringBootApplication注解,所有的奥秘,都只能在这两个点上了。
但是,在分析这个启动类之前,我们还需要先了解下springboot打包后产生的jar包。但是如果你比较着急想知道spingboot自动配置相关内容,那就直接去看第二节。
一. FAT jar
我们都知道,springboot打包后的jar文案,可以使用 java -jar x.jar来运行该jar文件,那么是如何执行的呢,怎么就执行了我们业务中的启动类中的main方法呢?
spring 打包后产生的jar包,与平时我们maven依赖的jar包有所不同,专业的术语叫做FAT jar,也就是胖jar,顾名思义,它要比普通jar胖一点,也就是内容多一点,我们解压两种jar看看内容:
随便打开一个jar包,比如gson-2.8.6.jar ,如下:
然后springboot的fat jar (名字是demo.jar)如下:
区别
很明显,也很简单,Fat jar 多了一个BOOT-INF目录,其他一样(com和org一样,都是包路径以及内部包含一些类文件)。
1.1 BOOT-INF
BOOT-INF里面的内容很明显,是我们的业务类文件以及我们业务依赖的一些jar包,jar在lib中。
1.2 META-INF
META-INF是jar文件规范里面要求的内容,包含MANIFEST.MF文件,我们打开springboot的jar中的MANIFEST.MF文件,如下:
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.0.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
关于此文件的作用以及使用规范,本文不在展开,可以参考相关文章:https://www.cnblogs.com/EasonJim/p/6485677.html
其中,如下两个属性需要注意:
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.demo.DemoApplication
Main-Class才是springboot真正的程序入口,一旦定义了此属性,就可以使用 java -jar x.jar来运行该jar文件。同时也表明,java -jar x.jar的执行方式,并不是springboot专有的,你也可以定义打包插件,让打出的包中包含你自己定义的JarLauncher.class,然后按照你自定义的方式运行程序。
springboot在JarLauncher中,调用了Start-Class,也就是我们工程代码中的启动类。
再往后,才执行工程中的main方法。
注意下,这个JarLauncher.class是打包在jar文件中的,如果想在工程中查看源码,需要增加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
二.main方法
这里先简单总结下,然后再分开详细说;
1.main方法里的SpringApplication.run肯定是springboot的程序入口,主要做了处理应用上下文ConfigurableApplicationContext等操作,并且刚才说的@SpringBootApplication注解,也将在这里被触发;
2.@SpringBootApplication注解用来导入外部模块的配置类,这里的模块是指在创建springboot项目时,选择的模块,比如web模块、数据库层的jpa模块等,体现在pom.xml的依赖上:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
还是先说@SpringBootApplication注解的机制吧,比如具体怎么导入初始化配置的;至于这个注解什么时候触发执行的,放在后边的SpringApplication.run()方法中说;
2.1.@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 {
几个传统注解不单独说了,下面重点说下其余2个:
1.@SpringBootConfiguration,打开
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
没啥东西,等同于@Configuration;
2.@EnableAutoConfiguration,所有奥秘都在这个注解里了,打开:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)//这里是重点
public @interface EnableAutoConfiguration {
先说明下,不懂@import注解的作用的话,很难继续往下理解了,import注解就是用来导入配置文件的,怎么用,具体看另外一个帖子:《spring @Import注解的作用和几种使用方式》
继续说:AutoConfigurationImportSelector这个类,打开,
@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()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(getAutoConfigurationMetadata(),
annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
找到上面重写的process方法,继续按照这个调用链路:getAutoConfigurationEntry()->getCandidateConfigurations()->
SpringFactoriesLoader.loadFactoryNames()->SpringFactoriesLoader.loadSpringFactories,地方到了
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));//看这里!!!
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
就是从META-INF/spring.factories这个路径下,读取配置文件; 我们在demo工程里搜一下这个文件,发现好多jar包里面都有:
回过头,在看下SpringFactoriesLoader.loadFactoryNames的入参,第一个参数值是getSpringFactoriesLoaderFactoryClass(),
打开看下,
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
发现是EnableAutoConfiguration这个类;也就是说,加载的就是spring.factories中key为EnableAutoConfiguration这个类的配置项;
springboot这种约定机制,给我们扩展功能带来方便;比如我们项目中自己开发公共的代码部分,可以打包成一个jar包,里面也可以加上这个文件,里面写一些我们想初始化的类;这样其他项目引用了自定义的jar包,一些需要的配置就会被sprinboot读取到;
打开springboot的默认配置文件:
里面的存储结构是key=value的形式,value可以写多个,用逗号隔开;
可以看到,springboot默认加载了es、jpa、redis等非常多的功能模块;
打开其中的org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,我们看下springboot是怎么配置redis参数的;
这个类上有个@EnableConfigurationProperties(RedisProperties.class),打开RedisProperties,
可以发现,springboot读取了配置文件中前缀是spring.redis的配置参数;
@SpringBootApplication就先粗浅的写这些吧;
2.2.Run方法执行流程
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return (new SpringApplication(primarySources)).run(args);
}
new了一个SpringApplication对象,然后执行run方法;
先看下构造函数:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
1.这里入参primarySources就是启动类本身,那么后边代码就能通过反射获取到这个类中的所有内容,主要是注解信息;
2.setInitializers,设置初始化类,用的依然是SpringFactoriesLoader,加载spring.factories文件中,key是ApplicationContextInitializer类的配置项目;
3.setListeners,同样的,加载spring.factories文件中,key是ApplicationListener的配置项目;
继续看run方法:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
Collection exceptionReporters;
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
//Initializer类在这里被执行!!!!
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//看这里!!!
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
listeners.started(context);
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
try {
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
挑重点说,因为主要想和上边的@SpringBootApplication注解串起来,其他的后边在慢慢说,贪多嚼不烂;
我们refreshContext()这个方法,打开,
private void refreshContext(ConfigurableApplicationContext context) {
this.refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException var3) {
;
}
}
}
继续打开refresh方法,
protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext)applicationContext).refresh();
}
继续打开refresh,因为这个applicationContext是ServletWebServerApplicationContext类型的,因此进入ServletWebServerApplicationContext.refresh()方法:
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
stopAndReleaseWebServer();
throw ex;
}
}
super.refresh(),父类是AbstractApplicationContext,也就是执行AbstractApplicationContext.refresh()方法,
到这里,已经不再是springboot的代码了,已经进入了spring的代码中,
public void refresh() throws BeansException, IllegalStateException {
Object var1 = this.startupShutdownMonitor;
synchronized(this.startupShutdownMonitor) {
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
//看这里
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var9) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
}
this.destroyBeans();
this.cancelRefresh(var9);
throw var9;
} finally {
this.resetCommonCaches();
}
}
}
invokeBeanFactoryPostProcessors执行后,就会走到@EnableAutoConfiguration注解中@import的AutoConfigurationImportSelector类的process方法中;至此,run方法和启动类上的注解就关联起来了;
其他@EnableXX的注解发挥作用的流程,也是一样的道理,关键就是那个@import注解。
refresh()方法中的具体内容,参考spring最新版本代码分析《spring5源码阅读(二)refresh()方法》
内嵌tomcat
主线内容还差一点,继续:
this.finishRefresh();在执行的时候,由于ServletWebServerApplicationContext作为子类,重写了此方法:
@Override
protected void finishRefresh() {
super.finishRefresh();
WebServer webServer = startWebServer();
if (webServer != null) {
publishEvent(new ServletWebServerInitializedEvent(webServer, this));
}
}
子类在执行完父类的super.finishRefresh();后,还额外执行了
WebServer webServer = startWebServer();
跟进去,你就会发现,springboot内嵌的tomcat,就是在这里触发启动的。
先记录到这里吧,springboot内容还是比较多的,先挑主线,后期再慢慢丰富内容。