SpringBoot原理总结

开始使用 SpringBoot

使用 sdkman 搭建环境

介绍

  1. sdkman
    工具包管理器,可以用于管理 Spring Boot CLI。
  2. Spring Boot CLI
    命令行工具。
  3. groovy
    一种可以运行在 JVM 上的语言,和 Java 的编译器不同(语法、语义不同)。

安装

  1. 安装 sdkman
    按下面链接中的步骤来,可能需要连接 VPN:
    http://sdkman.io/install.html
  2. 安装 Spring Boot CLI
    默认安装最新版本的(安装位置在$HOME/.sdkman 下):
    1
    sdk install springboot
  3. 切换 Spring Boot CLI 版本
    1
    2
    3
    sdk list springboot
    sdk install springboot XXX.RELEASE
    sdk use springboot XXX.RELEASE
  4. 设置默认版本
    1
    sdk default springboot XXX.RELEASE
  5. 使用本地编译的 springboot
    1
    2
    3
    $ sdk install springboot dev /path/to/spring-boot/spring-boot-cli/target/spring-boot-cli-2.0.0.BUILD-SNAPSHOT-bin/spring-2.0.0.BUILD-SNAPSHOT/
    $ sdk default springboot dev
    $ spring --version

第一个 SpringBoot 应用

  1. 创建一个 app.groovy 文件:
    1
    2
    3
    4
    5
    6
    7
    @RestController
    class ThisWillActuallyRun {
    @RequestMapping("/")
    String home() {
    "Hello World!"
    }
    }
  2. 执行
    1
    spring run app.groovy
  3. 访问
    1
    localhost:8080

IDEA 下搭建 SpringBoot 项目

创建一个简单的 Web 项目

  1. 创建项目
    new Projects… -> Spring Initializr(这里可能需要连接 VPN) -> 设置项目元数据 -> 添加 Web 组件(对于这个例子来说足够了)
  2. 添加控制器
    创建 controller.HelloController:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
    System.out.println("Hello");
    return "hello";
    }
    }
  3. 执行 XxxApplication.main()
  4. 访问
    localhost:8080

添加 Jpa 支持

  1. 添加依赖
    1
    2
    3
    4
    5
    6
    7
    8
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
  2. 添加配置
    1
    2
    3
    4
    5
    6
    7
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/test
    spring.datasource.username=root
    spring.datasource.password=85382855

    spring.jpa.hibernate.ddl-auto=create
    spring.jpa.show-sql=true
  3. 创建实体类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Entity
    public class User {
    @Id
    @GeneratedValue
    private Integer id;
    private String username;
    private String password;
    ...
    }
  4. 创建 Dao 接口
    1
    2
    public interface UserDao extends JpaRepository<User, Integer> {
    }
  5. 注入 Dao 接口
    1
    2
    @Autowired
    private UserDao userDao;

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloTest {

private MockMvc mockMvc;
@Before
public void setUp() throws Exception {
//测试 HelloWorldController
mockMvc = MockMvcBuilders.standaloneSetup(new HelloWorldController()).build();
}
@Test
public void getHello() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/hello?name=小明")
.accept(MediaType.APPLICATION_JSON_UTF8)).andDo(print());
}
}

SpringBoot 原理分析

核心思想

  1. 约定优于配置(convention over configuration)
    传统 Spring 项目配置繁琐是 SpringBoot 主要解决的痛点之一,autoconfigure 模块会在启动时为自动注入的 Bean 设置默认配置。
  2. 模块化
    类似于 maven 管理项目的依赖、git 管理代码的版本,SpringBoot 提供了对一些常用组件的模块化管理,只要添加依赖几乎就可以马上使用,展现出一种插件化的效果。

spring-boot-load 模块

正常情况下一个类加载器只能找到加载路径的 jar 包里当前目录或者文件类里面的 *.class 文件,SpringBoot 允许我们使用 java -jar archive.jar 运行包含嵌套依赖 jar 的 jar 或者 war 文件。

传统类加载器的局限

传统类加载器无法加载 jar 包中嵌套的 jar 包。比如项目打包后包含如下这些类和 jar 包,则 c.jar 中的类无法被加载:

1
2
3
4
5
6
a1.class
a2.class
b.jar
b1.class
b2.class
c.jar

为了能够加载嵌套 jar 里面的资源,之前的做法都是把嵌套 jar 里面的 class 文件和应用的 class 文件打包为一个 jar,这样就不存在嵌套 jar 了,但是这样做就不能很清晰的知道哪些是应用自己的,哪些是应用依赖的,另外多个嵌套 jar 里面的 class 文件可能内容不一样但是文件名却一样时候又会引发新的问题。

加载嵌套 jar 包中的 class 文件

在 Java 中,AppClassLoader 和 ExtClassLoader 都继承自 URLClassLoader,并通过构造函数来传递需要加载的 class 文件所在的目录。
那么只要将 jar 包中嵌套的 jar 包所在的路径作为 URLClassLoader 的扫描路径,就可以实现对嵌套 jar 包的扫描了。
但是默认情况下 Java 使用 AppClassLoader 加载使用命令启动时指定的 classpath 下的类和 jar 包,那么如何使用自定义的 URLClassLoader 来加载这些类和 jar 包呢?具体做法是加一个中间层。
原来是直接使用 AppClassLoader 来加载应用:AppClassLoader -> Application。
现在先加载一个自定义的启动类 Launcher,其中自定义 URLClassLoader,再使用该 URLClassLoader 来加载我们真正的 main 函数:AppClassLoader -> Launcher -> URLClassLoader -> Application。

spring-boot-load 提供的启动器 Launcher

spring-boot-load 模块允许我们加载嵌套 jar 包中的 class 文件,它包含三种类启动器:

  1. JarLauncher
  2. WarLauncher
  3. PropertiesLauncher

spring-boot-maven-plugin 打包插件及 SpringBoot 指定的包结构

为了为 SpringBoot 应用打包,需要引入如下 Maven 插件:

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.9.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

SpringBoot项目打包后的jar包结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
archive.jar
|
+-META-INF(1)
| +-MANIFEST.MF
+-org(2)
| +-springframework
| +-boot
| +-loader
| +-<spring boot loader classes>
+-com(3)
| +-mycompany
| + project
| +-YouClasses.class
+-lib(4)
+-dependency1.jar
+-dependency2.jar

目录(1)是 jar 文件中 MANIFEST.MF 文件的存放处。
目录(2)是 Spring-boot-loader 本身需要的 class 放置处。
目录(3)是应用本身的文件资源放置处。
目录(4)是应用依赖的 jar 固定放置处,即 lib 目录。

打包按如下顺序进行:

  1. 执行mvn clean package按原流程进行打包生成 jar 文件;
  2. spring-boot-maven-plugin 插件对 jar 包进行重写。
    1. 改写 MANIFEST.MF 文件:Main-Class: org.springframework.boot.loader.JarLauncherStart-Class: com.abc.MyApplication,这里的 MyApplication 即项目中被@SpringBootApplication注解了的那个类,指定主类后 AppClassLoader 就会先加载 JarLauncher 并执行其中的 main 方法了;
    2. 写入应用依赖的 jar 包到 lib 目录;
    3. 拷贝 spring-boot-load 包里的 class 文件到 jar 包的目录(2)处。

      为什么要拷贝到目录(2)的位置?因为后面启动流程中LaunchedURLClassLoader位于spring-boot-loader.jar包内,AppClassLoader无法加载嵌套的jar中的文件。

启动顺序如下,以 JarLauncher 为例,假设 SpringBoot 项目打包后的 jar 包名为 test.jar:
JarLauncher执行时序图

  1. 使用 AppClassLoader 加载 JarLauncher,执行其 main 函数;
  2. 查找 test.jar 中的嵌套 jar 后生成一个列表List<Archive>,每个Archive包含了一个嵌套 jar 的信息,对应 jar 包 Archive 的子类是 JarFileArchive
  3. 将 test.jar 本身构造成的 Archive 插入List<Archive>列表的第一个位置;
  4. 将这个列表作为参数构建LaunchedURLClassLoader类加载器,这个类加载器继承自 URLClassLoader;
  5. 使用 LaunchedURLClassLoader 加载并实例化 MyApplication 的一个对象,并通过反射调用 main 函数,这时候 SpringBoot 才正式开始初始化 SpringBoot 的环境,并创建容器。

spring-boot-autoconfigure 模块

Spring 的出现给我们管理 Bean 的依赖注入提供了便捷,但是当我们需要使用通过 pom 引入的 jar 里面的一个 Bean 时候,还是需要手动在 XML 配置文件里面配置。
spring-boot-autoconfigure 思路类似 **SPI(Service Provider Interface)**,都是不同的实现类实现了定义的接口,加载时候去查找 classpath 下的实现类,不同在于前者使用 autoconfigure 实现,后者使用的是 ServiceLoader
autoconfigure 模块的主要功能如下:

  1. 可以依据 classpath 里面的依赖内容自动配置 Bean 到 Spring 容器,并为默认化属性配置。
  2. 尝试推断哪些 Beans 是用户可能会需要的。比如如果 HSQLDB 包在当前 classpath 下,并且用户并没有配置其他数据库链接,这时候 Auto-configuration 功能会自动注入一个基于内存的数据库连接到应用的 IOC 容器。再比如当我们在 pom 引入了 Tomcat 的 start 后,如果当前还没 Web 容器被注入到应用 IOC,那么 SpringBoot 就会为我们自动创建并启动一个内嵌 Tomcat 容器来服务。

EnableAutoConfiguration

核心注解@EnableAutoConfiguration的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

EnableAutoConfiguration 注解主要作用是通过 @Import 注入 EnableAutoConfigurationImportSelector 实例,后者是实现 AutoConfiguration 功能的核心类。
EnableAutoConfigurationImportSelector时序图
EnableAutoConfigurationImportSelector 类实现了 DeferredImportSelector 接口的 String[] selectImports(AnnotationMetadata metadata) 方法,当 Spring 框架解析 @Import 注解时候会调用该方法。
代码EnableAutoConfigurationImportSelector#getCandidateConfigurationsloadFactoryNames的作用是扫描 classpath 下含有 Meta-INF/spring.factories 文件的 jar,并解析文件中名字为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的配置项。
getCandidateConfigurations会返回名字为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的配置项的值的一个列表并且对列表内容进行去重处理。

正常情况下,去重后的列表里面的类都会被自动注入到 Spring IOC 容器,除非指定不自动注入某些功能,比如若要排除自动创建数据源和事务管理的功能可以:@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class}),getExclusions 就是读取 EnableAutoConfiguration 注解里面的 excludeName 和 exclude 配置项,然后从去重后的列名列表里面剔除。这样在自动注入时候就不会对排除掉的自动配置功能进行注入了。

这个列表中的每个元素就是一个自动配置功能,比如 org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration 是 Web 容器自动注入的功能类(比如这个类可以自动扫描 Tomcat 的 start 并创建一个 Tomcat 容器)。

例子 - 注入 Web 容器

ServletWebServerFactoryAutoConfiguration 的部分核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

@Bean
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new ServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}

/**
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
* {@link ImportBeanDefinitionRegistrar} for early registration.
*/
public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {

private ConfigurableListableBeanFactory beanFactory;

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
}

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
if (this.beanFactory == null) {
return;
}
registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
WebServerFactoryCustomizerBeanPostProcessor.class);
registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor",
ErrorPageRegistrarBeanPostProcessor.class);
}

private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class<?> beanClass) {
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(name, beanDefinition);
}
}

}

}

ServletWebServerFactoryAutoConfiguration 类是 Web 容器自动注入的 Auto-configuration 类。

  • @ConditionalOnWebApplication 说明当前是 Web 环境上下文时候才注入本类到 IOC。
  • @Import 注解导入了另一个配置ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,这里包含了真正的注入 Tomcat 实例的逻辑:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Configuration
    class ServletWebServerFactoryConfiguration {

    @Configuration
    @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
    @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedTomcat {

    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
    return new TomcatServletWebServerFactory();
    }

    }
    ...
  • @ConditionalOnClass:只有在 classpath 下存在指定的类的时候 Bean 才能被创建。
  • @ConditionalOnMissingBean:只有对应的 Bean 在系统中都没有被创建,它修饰的初始化代码块才会执行,【用户自己手动创建的 Bean 优先】。

当应用引入 spring-boot-starter-web 时候默认引入的是 Tomcat 的 starter,所以会发现 classpath 下存在 Servlet.class、 Tomcat.class 这两个类,并且 IOC 里面没有 TomcatEmbeddedServletContainerFactory 的实例,因此会创建其实例到 IOC 容器中,最终会创建一个 Tomcat 容器。
当然,如果需要使用 Jetty 则需要在引用 spring-boot-starter-web 的时候排除掉 Tomcat 的 start,然后在引入 Jetty 的 start 即可。

总结

总而言之,自动配置的生效步骤可以概括如下:

  1. Spring Boot 在启动时会去依赖的 Starter 包中寻找 resources/META-INF/spring.factories 文件,然后根据文件中配置的 Jar 包去扫描项目所依赖的 Jar 包。
  2. 根据 spring.factories 配置加载 AutoConfigure 类
  3. 根据 @Conditional 注解的条件,进行自动配置并将 Bean 注入 Spring Context

spring-boot 模块

提供了一些特性用来支持 SpringBoot 中其它模块,这些特性包括:

  1. SpringApplication 类提供了静态方法以便于写一个独立了 Spring 应用程序,该类的主要职责是 create 和 refresh 一个合适的 Spring 应用程序上下文(ApplicationContext)。
  2. 一流的外部配置的支持(application.properties)。
  3. 提供了便捷的应用程序上下文(ApplicationContext)的初始化器,以便在 ApplicationContext 使用前对其进行用户定制。
  4. 给 Web 应用提供了一个可选的 Web 容器(目前有 Tomcat 或 Jetty)。

SpringApplication 的执行流程

一般的 SpringBoot 项目都会有一个如下所示的 Application 类:

1
2
3
4
5
6
7
@SpringBootApplication
public class Web1Application {

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

JarLauncher启动过后里面实际上就是调用了 SpringApplication#run 方法,SpringApplication 的执行流程如下:
SpringApplication执行时序图

  1. SpringApplication 的构造函数里面会调用 initialize 方法在 classpath 的 jar 包里面查找 META-INF/spring.factories,如果找到则看里面是否有配置 ApplicationContextInitializer 类型的 Bean,有则加载到 SpringApplication 的变量 initializers 里面存放,比如 spring-boot.jar 里面。
    ApplicationContextInitializer配置
  2. createAndRefreshContext 做了这几件事情:
    1. 设置环境,加载 application.properties 等配置文件;
    2. 根据 classpath 的 jar 里面是否有 ConfigurableWebEnvironment 判断当前是否需要创建 Web 应用程序上下文还是创建一个非 Web 应用程序上下文;
    3. 使用前面加载的应用程序初始化器对创建的应用程序上下文进行初始化;
    4. 刷新应用程序上下文解析 Bean 定义到应用程序上下文里面的 IOC 容器,在刷新过程的 invokeBeanFactoryPostProcessors 过程中(10)(11)会去解析类上面标注的 @import 注解,然后就会调用所有的 ImportSelector 的 selectImports 方法,这也是第二部分时序图开始执行的地方。

Web 容器的创建

通过自动配置将 TomcatEmbeddedServletContainerFactory 或者 JettyEmbeddedServletContainerFactory 的实例注入 IOC 容器后,我们就可以使用 Web 容器来提供 Web 服务了。
具体的 Web 容器的创建是在容器刷新过程的 onRefresh 阶段进行的(这个阶段是在刷新过程的 invokeBeanFactoryPostProcessors 阶段的后面):
Web容器创建过程

  • getBeanNamesForType 获取了 IOC 容器中的 EmbeddedServletContainerFactory 类型的 Bean 的 name 集合,如果 name 集合为空或者多个则抛出异常。还记得 Web 容器工厂是通过自动配置注入到 IOC 的吧,并且 TomcatEmbeddedServletContainerFactory 或者 JettyEmbeddedServletContainerFactory 都是实现了 EmbeddedServletContainerFactory 接口。
  • 如果 IOC 里面只有一个 Web 容器工厂 Bean 则获取该 Bean 的实例,然后调用该 Bean 的 getEmbeddedServletContainer 获取 Web 容器,这里假设 Web 容器工厂为 Tomcat ,则创建 Tomcat 容器并进行初始化。

Actuator 模块

Actuator类图
Actuator 模块提供了一些组件用于检查应用或中间件的健康状况:

  1. Inspect:检测应用或中间件的健康状况;
  2. Aggregate:聚合所有结果。

基于 Actuator 模块,我们可以开发一个服务探活系统:

  1. Actuator 为业务服务器暴露出一个探活端口healthcheck,借助 Spring 容器获取到中间件的客户端实例;
  2. 探活服务HealthCheck-Task定时轮询所有服务,统计单位时间内请求出现问题的次数,从而发送报警短信提醒研发同学。

服务的轮询探活将是这个系统的主要瓶颈,有一些措施可以作为参考:

  1. 灵活使用CompletableFuture这些并发编排组件,因为探活请求一般都是比较”慢”的,因此可以通过并行化来提高效率;
  2. 探活不能占用太多系统资源,合理设置探活超时时间,很多客户端的默认超时时间达到 1 分钟甚至更长,必须额外设置探活请求的超时时间,避免服务器挂掉时所有线程都被hang死,导致服务雪崩。

    探活不能和业务代码共用线程池,做到线程池隔离,即使出问题也只影响到HealthCheck-Task

参考

原理

  1. SpringBoot应用篇(一):自定义starter
  2. 从零开始开发一个Spring Boot Starter
  3. SpringBoot启动流程解析

源码分析

  1. fangjian0423/springboot-analysis