一、基础
1. 什么是Spring Boot
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.
We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.
使用“习惯优于配置”(项目中存在大量的配置,此外还内置了一个习惯性的配置,让你无需手动配置)的理念让你的项目快速运行起来。
特征
- 创建独立的Spring应用程序;
- 直接嵌入Tomcat,Jetty或Undertow(无需部署WAR文件);
- 提供“初始”的POM文件内容,以简化Maven配置;
- 尽可能自动配置Spring;
- 提供生产就绪的功能,如指标,健康检查和外部化配置;
- 绝对无代码生成,也不需要XML配置。
构建
- Spring Boot父级依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
有了Spring Boot父级依赖,表示当前的项目就是Spring Boot项目了,spring-boot-starter-parent是一个特殊的starter,它用来提供相关的Maven默认依赖,使用它之后,常用的包依赖可以省去version标签。
- Spring Boot Maven插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐maven‐plugin</artifactId>
</plugin>
</plugins>
</build>
Spring Boot Maven插件提供了许多方便的功能:
- 把项目打包成一个可执行的超级JAR(uber-JAR),包括把应用程序的所有依赖打入JAR文件内,并为JAR添加一个描述文件,其中的内容能让你用java -jar来运行应用程序。
- 搜索public static void main()方法来标记为可运行类。
二、原理
2.1 核心注解
2.1.1 @SpringBootApplication
@Target(ElementType.TYPE) // 注解的适用范围,其中TYPE用于描述类、接口(包括包注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期,保留到class文件中(三个生命周期)
@Documented // 表明这个注解应该被javadoc记录
@Inherited // 子类可以继承该注解
@SpringBootConfiguration // 继承了Configuration,表示当前是注解类
@EnableAutoConfiguration // 开启springboot的注解功能,springboot的四大神器之一,其借助@import的帮助
@ComponentScan(excludeFilters = {// 扫描路径设置)
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes =
AutoConfigurationExcludeFilter.class)})
public @interface SpringBootApplication {
...
}
注解三剑客:
- @SpringBootConfiguration,即(Configuration)
- @EnableAutoConfiguration
- @ComponentScan
SpringBootConfiguration
里面只有一个@Configuration注解。
它就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@Configuration,SpringBoot社区推荐使用基于JavaConfig的配置形式,所以,这里的启动类标注了@Configuration之后,本身其实也是一个IoC容器的配置类。
- 任何一个标注了@Configuration的Java类定义都是一个JavaConfig配置类。
@Configuration
public class MockConfiguration{
//bean定义
}
- 任何一个标注了@Bean的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。
@Configuration
public class MockConfiguration{
@Bean
public MockService mockService(){
return new MockServiceImpl();
}
}
- 如果一个bean的定义依赖其他bean,则直接调用对应的JavaConfig类中依赖bean的创建方法就可以了。
@Configuration
public class MockConfiguration{
@Bean
public MockService mockService(){
return new MockServiceImpl(dependencyService());
}
@Bean
public DependencyService dependencyService(){
return new DependencyServiceImpl();
}
}
ComponentScan
@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。
我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。(默认不指定basePackages)
EnableAutoConfiguration(重中之重)
借助@Import的支持,收集和注册特定场景相关的bean定义,将所有符合自动配置条件的bean定义加载到IoC容器。
@Target({java.lang.annotation.ElementType.TYPE})
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
java.lang.String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
java.lang.Class<?>[] exclude() default {};
java.lang.String[] excludeName() default {};
}
@AutoConfigurationPackage:自动配置包
作用是将 添加该注解的类所在的package 作为 自动配置package 进行管理。
// 它其实是注册了一个Bean的定义。
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
register(
registry,
new PackageImport(metadata).getPackageName() // 返回当前主程序类的同级以及子级的包组件。(这也就是为什么,我们要把DemoApplication放在项目的最高级中。)
);
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
@Import(AutoConfigurationImportSelector.class): 导入自动配置的组件
AutoConfigurationImportSelector 继承了 DeferredImportSelector 继承了 ImportSelector。
// 它其实是去加载spring.factories外部文件,这个外部文件,有很多自动配置的类。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
// 读取spring.factories里面的配置类
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);
/*
* 通过SpringFactoriesLoader.loadFactoryNames读取spring.factories的配置类
* 格式为全路径包名(便于后续反射创建实例)
*/
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
/*
* 去重 new ArrayList<>(new LinkedHashSet<>(list));
*/
configurations = removeDuplicates(configurations);
/*
* 获取注解上exclude或者excludeName属性标记的需要排除的配置类,
* 以及spring.autoconfigure.exclude标记的需要排除的配置类
*/
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
/*
* 检查要排除的配置类是否在configurations中,如果不在则抛出IllegalStateException异常
*/
checkExcludedClasses(configurations, exclusions);
// configurations去除要排除的配置类
configurations.removeAll(exclusions);
/*
* 过滤在spring.factories里面配置了
* org.springframework.boot.autoconfigure.AutoConfigurationImportFilter
* 为key的配置类,实际是过滤有条件的bean创建。
*/
configurations = filter(configurations, autoConfigurationMetadata);
/*
* 在spring.factories里面查找配置了
* org.springframework.boot.autoconfigure.AutoConfigurationImportListener
* 为key的listener,并绑定AutoConfigurationImportEvent(包含配置类)
*/
fireAutoConfigurationImportEvents(configurations, exclusions);
// 封装成AutoConfigurationEntry,供run方法通过反射创建实例
return new AutoConfigurationEntry(configurations, exclusions);
}
}
其中,最关键的要属 @Import(AutoConfigurationImportSelector.class),借助
AutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。就像一只“八爪鱼”一样。
SpringFactoriesLoader详解
借助于Spring框架原有的一个工具类:SpringFactoriesLoader的支持,@EnableAutoConfiguration可以 “智能” 的自动配置功效才得以大功告成!
SpringFactoriesLoader属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
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()) {
List<String> factoryClassNames = Arrays.asList(
StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
result.addAll((String) entry.getKey(), factoryClassNames);
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
@EnableAutoConfiguration 的场景中,SpringFactoriesLoader提供了一种配置查找的功能支持,即根据 @EnableAutoConfiguration 的完整类名 org.springframework.boot.autoconfigure.EnableAutoConfiguration 作为查找的 Key,获取对应的一组 @Configuration 类。
总结:@EnableAutoConfiguration 自动配置其实就是从classpath 中搜寻所有 META-INF/spring.factories 配置文件,并将其中 org.spring-framework.boot.autoconfigure.EnableAutoConfiguration 对应的配置项通过反射实例化为对应的标注了 @Configuration 的 JavaConfig 形式的 IoC 容器配置类,然后汇总为一个并加载到 IoC 容器。
2.1.2 Spring Boot启动原理
- 如果我们使用的是SpringApplication的静态run方法,那么,这个方法里面首先要创建一个SpringApplication对象实例,然后调用这个创建好的SpringApplication的实例run方法。在SpringApplication实例初始化的时候,它会提前做几件事情:
- 根据 classpath 里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为 Web 应用使用的 ApplicationContext 类型,还是应该创建一个标准 Standalone 应用使用的 ApplicationContext 类型。
- 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationContextInitializer。
- 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationListener。
- 推断并设置 main 方法的定义类。
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
//把SpringdemoApplication.class设置为属性存储起来
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
//设置应用类型为Standard还是Web
this.webApplicationType = WebApplicationType.deduceFromClasspath();
//设置初始化器(Initializer),最后会调用这些初始化器
setInitializers((Collection)getSpringFactoriesInstances(ApplicationContextInitializer.class));
//设置监听器(Listener)
setListeners((Collection)getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
- SpringApplication实例初始化完成并且完成设置后,就开始执行run方法的逻辑了,方法执行伊始,首先遍历执行所有通过SpringFactoriesLoader可以查找到并加载的SpringApplicationRunListener。调用它们的started()方法,告诉这些SpringApplicationRunListener,“嘿,SpringBoot应用要开始执行咯!”。
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
//计时工具
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
//第一步:获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//第二步:根据SpringApplicationRunListeners以及参数来准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//准备Banner打印器‐就是启动Spring Boot的时候在console上的ASCII艺术字体
Banner printedBanner = printBanner(environment);
//第三步:创建Spring容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
//第四步:Spring容器前置处理
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//第五步:刷新容器
refreshContext(context);
//第六步:Spring容器后置处理
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//第七步:发出结束执行的事件
listeners.started(context);
//第八步:执行Runners
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
//返回容器
return context;
}
- 创建并配置当前 SpringBoot 应用将要使用的 Environment(包括配置要使用的 PropertySource 以及 Profile)。
- Environment是SpringFramework中一个很重要的接口,用于存放应用程序的配置信息。
- PropertySource(org.springframework.core.env.PropertySource)是用来将一个对象以键值对的形式表示,Spring将多种来源的属性键值对转换成PropertySource来表示。
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
/*
* 一、创建Environment对象。
* 在getOrCreateEnvironment方法中,会根据之前推断的webApplicationType(web程序类型)创建不同了实现的Environment对象
*/
ConfigurableEnvironment environment = getOrCreateEnvironment();
/*
* 二、配置Environment对象。
* 1. 应用程序如果有命令行参数,则在Environment中添加一个与这个命令行参数相关的PropertySource;
* 2. 根据命令行参数中spring.profiles.active属性配置Environment对象中的activeProfile。
*/
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 三、发布ApplicationEnvironmentPreparedEvent(应用环境已准备)事件。
listeners.environmentPrepared(environment);
/*
* 四、将Environment中的spring.main属性绑定到SpringAppilcation对象中,在执行到这一步时,Environment中已经包含了用户设置的profile文件属性。
*/
bindToSpringApplication(environment);
/*
* 五、转换Environment对象的类型。
* 在上一步中,如果用户使用spring.main.web-application-type属性手动设置了应用程序的webApplicationType并且用户设置的类型与SpringApplication推断出来的不一致,则
SpringApplication会将环境对象转换成用户设置的webApplicationType相关的类型。
*/
if (this.webApplicationType == WebApplicationType.NONE) {
environment = new EnvironmentConverter(getClassLoader())
.convertToStandardEnvironmentIfNecessary(environment);
}
ConfigurationPropertySources.attach(environment);
return environment;
}
- 遍历调用所有SpringApplicationRunListener的environmentPrepared()的方法,告诉他
们:“当前SpringBoot应用使用的Environment准备好了咯!”。
public void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.environmentPrepared(environment);
}
}
- 如果SpringApplication的showBanner属性被设置为true,则打印banner。
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null ? this.resourceLoader
: new DefaultResourceLoader(getClassLoader()));
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
resourceLoader, this.banner);
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
- 创建ApplicationContext,根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用创建什么类型的ApplicationContext(通过webApplicationType类型判断),最后通过BeanUtils实例化上下文对象,并返回。
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, "
+ "please specify an ApplicationContextClass",
ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
- SpringApplication在prepareContext方法中对上下文对象进行预配置
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
/*
* 发布ApplicationContextInitializedEvent(上下文已初始化)事件。
* 执行所有ApplicationContextInitializer的initialize方法。
* 这些ApplicationContextInitializer是在SpringApplication中的构造函数中加载的(通过读取spring.factories加载)
*/
applyInitializers(context);
/*
* 发布ApplicationPreparedEvent(上下文已准备)事件。
*/
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
context.getBeanFactory().registerSingleton("springApplicationArguments",
applicationArguments);
if (printedBanner != null) {
context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
- 刷新应用上下文对象
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
- 发布应用程序已启动(ApplicationStartedEvent)事件
- 在BeanFactory中获取所有ApplicationRunner和CommandLineRunner并调用他们的run方法
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
- 异常处理,如果run方法的处理过程中发生异常,则对exitCode进行相应处理
private void handleRunFailure(ConfigurableApplicationContext context,
Throwable exception,
Collection<SpringBootExceptionReporter> exceptionReporters,
SpringApplicationRunListeners listeners) {
try {
try {
handleExitCode(context, exception);
if (listeners != null) {
listeners.failed(context, exception);
}
}
finally {
reportFailure(exceptionReporters, exception);
if (context != null) {
context.close();
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
ReflectionUtils.rethrowRuntimeException(exception);
}
SpringBoot应用启动步骤简要示意图如下:
- shutdownHook简单介绍 [^1]
- 作用:JVM退出时执行的业务逻辑
- 添加:Runtime.getRuntime().addShutdownHook()
- 移除:Runtime.getRuntime().removeShutdownHook(this.shutdownHook)
- *什么时候会调用Shutdown Hook
- 程序正常停止
- 程序异常退出
- 受到外界影响停止
2.1.3 配置静态资源地址和访问路径
spring.mvc.static-path-pattern
spring.mvc.static-path-pattern代表的含义是我们应该以什么样的路径来访问静态资源,换句话说,只有静态资源满足什么样的匹配条件,Spring Boot才会处理静态资源请求,以官方配置为例:
# 这表示只有静态资源的访问路径为/resources/**时,才会处理请求
spring.mvc.static‐path‐pattern=/resources/**
请求地址类似于“http://localhost:8080/resources/jquery.js” 时,Spring Boot才会处理此请求,处理方式是将根据模式匹配后的文件名查找本地文件,那么应该在什么地方查找本地文件呢?这就是“spring.resources.static-locations”的作用了。
spring.resources.static-locations
spring.resources.static-locations”用于告诉Spring Boot应该在何处查找静态资源文件,这是一个列表性的配置,查找文件时会依赖于配置的先后顺序依次进行,默认的官方配置如下:
spring.resources.static‐locations=classpath:/static,classpath:/public,classpath:/resources,classpath:/META‐INF/resources
静态资源的Bean配置
xml配置如下:
<mvc:resources mapping="/resources/**" location="/public‐resources/">
<mvc:cache‐control max‐age="3600" cache‐public="true"/>
</mvc:resources>
java config配置如下:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public‐resources/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)
.cachePublic());
}
}
综上,“spring.mvc.static-path-pattern”用于阐述HTTP请求地址,而“spring.resources.staticlocations”则用于描述静态资源的存放位置。