什么是 Spring Boot

Spring Boot 基本上是 Spring 框架的扩展,它消除了设置 Spring 应用程序所需的复杂例行配置。我们在使用 Spring 框架的时候,我们接触得比较多的应该是 Spring MVC、 IOC 、 DI 、AOP 等等,而这些框架在使用的过程中会需要配置大量的 XML,或者需要做很多繁琐的配置。Spring Boot 可以帮助我们快速搭建一个基于 Spirng 框架以及 Spring 生态体系的应用解决方案。
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图1 我们对着官网来翻译一下:

  • 创建独立的 Spring 应用程序
  • 直接嵌入Tomcat,Jetty 或 Undertow(无需部署 WAR 文件),java -jar 就可以运行
  • 提供 starter 依赖来简化你自己的配置
  • 自动装配 Spring 和第三方的依赖只要可能
  • 提供可用于生产的功能,例如指标,运行状况检查和外部化配置,比如 Actuator
  • 完全没有代码生成,也不需要 XML 配置

看了上面这么多主要有两点,约定大于配置自动装配

约定大于配置

约定优于配置的体现主要是

  1. maven 的目录结构,默认有 resources 文件夹存放配置文件,默认打包方式为 jar
  2. spring-boot-starter-web 中默认包含 spring mvc 相关依赖以及内置的 tomcat 容器,使得构建一个 web 应用更加简单
  3. 默认提供 application.properties/yml 文件
  4. 默认通过 spring.profiles.active 属性来决定运行环境时读取的配置文件
  5. EnableAutoConfiguration 默认对于依赖的 starter 进行自动装配

    自动装配

    讲自动装配首先从注解开始,我们从 @SpringBootApplication 点进去
    可以看到它实际上是一个复合注解,上面四个是元注解,下面三个才是重点

  6. @Configuration

  7. @EnableAutoConfiguration
  8. @ComponentScan

我们可以直接用这三个注解也可以启动 Spring Boot 应用,只是每次配置三个注解比较繁琐,所以直接用一个复合注解更方便些。下面是官网的截图,刚兴趣的小伙伴可自行翻译。
image.svg

@Configuration

@Configuration 这个注解大家应该都用过,它是 JavaConfig 形式的基于 Spring IOC 容器的配置类使用的一种注解。所以在启动类里面标注了 @Configuration,意味着它其实也是一个 IoC 容器的配置类。
传统意义上的 Spring 应用都是基于 xml 形式来配置 bean 的依赖关系。但是从 Spring3 开始,Spring 就支持了两种 bean 的配置方式,一种是基于 xml 文件方式,另一种就是 JavaConfig,任何一个标注了@Configuration 的 Java 类定义都是一个JavaConfig 配置类。而在这个配置类中,任何标注了@Bean 的方法,它的返回值都会作为 Bean 定义注册到 Spring 的 IoC 容器,方法名默认成为这个 Bean 的 id。然后通过 spring 容器在启动的时候,把 Bean 进行初始化并且,如果 Bean 之间存在依赖关系,则分析这些已经在 IoC 容器中的 Bean 根据依赖关系进行组装。

@ComponentScan

@ComponentScan 这个注解大家也用过,这个很简单,就是扫包,相当于 xml 配置文件中的
< context:component-scan > 。 它的主要作用就是扫描指定路径下的标识了需要装配的类,自 动装配到 Spring 的 IoC 容器中。
标识需要装配的类的形式主要是:@Component、@Repository、@Service、@Controller这类的注解标识的类。(注:@Repository、@Service、@Controller 的底层还是 @Component)。 ComponentScan 默认会扫描当前 package 下的的所有加了相关注解标识的类到 IoC 容器中。

@EnableAutoConfiguration

好,主角登场了,@EnableAutoConfiguration 是 Spring Boot 的灵魂,是重中之重。从 Spring3.1 开始,提供了一系列的 @Enable 开头的注解,它是在 JavaConfig 框架上更进一步的完善,使用户在使用 Spring 相关的框架避免配置大量的代码从而降低使用的难度。
比如常见的一些 Enable 注解:@EnableWebMvc、@EnableScheduling、@EnableAsync 等等。 每一个涉及到 Enable 开头的注解,都会带有一个 @Import 的注解, @EnableAutoConfiguration 也不例外,我们点进去发现如红框所示。
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图3 @Import 注解是什么意思呢? 它对应 XML 形式下的< import resource/ >,就是导入资源,把多个分布在不同容器下的配置合并在一个配置中。@Import 注解可以配置三种不同的 class :

  1. 普通 Bean 或者带有 @Configuration 的配置文件
  2. 实现 ImportSelector 接口进行动态注入
  3. 实现 ImportBeanDefinitionRegistrar 接口进行动态注入

这里导入的是第二种 importSelector,这是一种动态注入 Bean 的技术,我们把AutoConfigurationImportSelector 点进去,发现它实现了 ImportSelector 接口。
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图4 找到实现方法selectImports ,该方法的作用就是找到相应的 Bean 注入到容器中。

  1. @Override
  2. public String[] selectImports(AnnotationMetadata annotationMetadata) {
  3. if (!isEnabled(annotationMetadata)) {
  4. return NO_IMPORTS;
  5. }
  6. AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
  7. .loadMetadata(this.beanClassLoader);
  8. AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
  9. annotationMetadata);
  10. return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
  11. }
  12. 复制代码

再从 getAutoConfigurationEntry 方法点进去,这里面做了许多事情,就是把找到的 Bean 进行排除、过滤、去重,我们可以看到 removeDuplicates、remove、filter 等方法。
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图5 那具体这些 Bean 从哪里找呢,我们将 getCandidateConfigurations 方法点进去,发现了一个惊天秘密,那就是在这有一个 META-INF/spring.factories 文件。【20200524】深度解析 Spring Boot 以及手写一个 stater - 图6 当然这是一个报错信息,我们不敢断定就是这里,没关系,我们把 SpringFactoriesLoader.loadFactoryNames 点进去,发现这里有个变量 FACTORIES_RESOURCE_LOCATION。【20200524】深度解析 Spring Boot 以及手写一个 stater - 图7 而这个变量的值还是 META-INF/spring.factories。 【20200524】深度解析 Spring Boot 以及手写一个 stater - 图8 看到这里我们很激动,于是我毫不犹豫的在项目中搜索这个文件,原来 SpringFactoriesLoader 的作用就是从 classpath/META-INF/spring.factories 文件中,根据 key来加载对应的类到 Spring IoC 容器中。 【20200524】深度解析 Spring Boot 以及手写一个 stater - 图9 看到这里小伙伴就明白了,就是把这么多Configuration下的 Bean 加载到容器里嘛,但是But,怎么还有 RabbitMQ、Elasticsearch这些我都用不到,怎么也给加到容器里来了,那多浪费空间和内存啊?小伙伴莫慌,于是我又带着好奇心理打开了 RabbitMQ 的配置类。【20200524】深度解析 Spring Boot 以及手写一个 stater - 图10 看到这里终于舒了口气,小伙们有没有发现这里多了一些 Conditional 的注解,其实这些就是条件注解,Spring Boot 也不傻,它会发现如果当前的 classpath 环境下没有相关联的依赖,则意味着这些类没必要进行加载。所以,通过这种条件过滤可以有效的减少 @configuration 类的数量从而降低 Spring Boot 的启动时间。

Conditions 描述
@ConditionalOnBean 在存在某个 bean 的时候
@ConditionalOnMissingBean 不存在某个 bean 的时候
@ConditionalOnClass 当前 classpath 可以找到某个类型的类时
@ConditionalOnMissingClass 当前 classpath 不可以找到某个类型的类时
@ConditionalOnResource 当前 classpath 是否存在某个资源文件
@ConditionalOnProperty 当前 jvm 是否包含某个系统属性为某个值
@ConditionalOnWebApplication 当前 spring context 是否是 web 应用程序

好,有了上面这么多预备知识后,就可以开始手写一个我们自己的 starter 了。

手写 starter

starter 工程的命名

starter 是一个开箱即用的组件,减少不必要的重复代码,重复配置。例如,如果要使用 Spring 和 JPA 进行数据库访问,在项目中引用spring-boot-starter-data-jpa 即可。
Spring 官方定义的 starter 通常命名遵循的格式为 spring-boot-starter-{name},例如 spring-boot-starter-web。非官方 starter 命名应遵循 {name}-spring-boot-starter 的格式,例如,dubbo-spring-boot-starter。
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图11

需求

写一个序列化的插件,并且可以自由的选择 fastjson 还是 gson,如果没选的情况下默认选择fastjson。

步骤

1、 创建一个Spring Boot项目,这里项目名字叫 jackformat-spring-boot-starter
image.svg
2、引入依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-autoconfigure</artifactId>
  4. </dependency>
  5. <!-- 这个是用来提示用的-->
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-configuration-processor</artifactId>
  9. <optional>true</optional>
  10. </dependency>
  11. <dependency>
  12. <groupId>com.alibaba</groupId>
  13. <artifactId>fastjson</artifactId>
  14. <version>1.2.56</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>com.google.code.gson</groupId>
  18. <artifactId>gson</artifactId>
  19. <version>2.2.4</version>
  20. </dependency>
  21. 复制代码

3、先定义一个格式化的接口,分别写两个实现类

  1. public interface FormatProcessor {
  2. /**
  3. * 定义一个格式化的方法
  4. *
  5. * @param obj
  6. * @param <T>
  7. * @return
  8. */
  9. <T> String format(T obj);
  10. }
  11. 复制代码

image.svgimage.svg 4、写一个配置类,这里用了条件注解,如果 fastjson 和 gson 类存在的情况下才加载对应的实现类,因为在 pom 文件里都引用了,所以这里都会被装载。注意这里红框标的 @Primary,对同一个接口,有几种不同的实现类时,@Autowired 是按类型注入的,不知道要选哪一个,按照第二点需求,用户在没选的情况下默认选择 fastjson,所以这里给 fastjson 的实现上打上 @Primary。 【20200524】深度解析 Spring Boot 以及手写一个 stater - 图15 5、配置类,用来读取用户的选择,作用和 @Value 一样,只是用了 jackxu.format 的前缀,这样更方便。【20200524】深度解析 Spring Boot 以及手写一个 stater - 图16 6、序列化实现类,这个就是提供给用户用来序列化用的,看名字 Template 大家也能知道,比如我们常用的 RedisTemplate、JdbcTemplate,构造函数的时候直接传入具体的实现。 【20200524】深度解析 Spring Boot 以及手写一个 stater - 图17 7、好,现在就是最关键的主类了,我们从上往下看,@Import 之前说过了,导入配置类,就是将该配置类中的 Bean 注入到容器,@EnableConfigurationProperties 这是在将属性类激活,注入到容器中,也可以用 @Bean 的方式,@Configuration 说明这是一个配置类。接下来将 FormatTemplate 注入到容器中,我们看到首先是去属性类中去读属性,如果是 fastjson 就返回 fastjson 的实现,如果是 gson 就返回 gson 的实现,如果没读取到,就用前面设置的 @Primary 的默认实现。【20200524】深度解析 Spring Boot 以及手写一个 stater - 图18 8、最后一步最关键的就是设置,在 resources 文件夹下创建 META-INF/spring.factories 文件,通过上面的知识,Spring Boot 在启动的时候就是读取该文件下的配置类,从而将 Bean 加载到容器中。【20200524】深度解析 Spring Boot 以及手写一个 stater - 图19

测试

1、将自己的 starter 项目进行 install 打包
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图20 2、测试项目中引用自己的 starter【20200524】深度解析 Spring Boot 以及手写一个 stater - 图21
3、写一个controller,一个测试类,并把 formatTemplate 注入进来
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图22
4、设置我们需要制定的序列化方式,这里选用 fastjson
【20200524】深度解析 Spring Boot 以及手写一个 stater - 图23 5、启动Spring Boot 项目【20200524】深度解析 Spring Boot 以及手写一个 stater - 图24 6、通过浏览器访问,发现这里显示的是 fastjson 方式的序列化,成功了!【20200524】深度解析 Spring Boot 以及手写一个 stater - 图25 7、在测试 gson 的方式,返回了默认 gson 的实现,也成功了!【20200524】深度解析 Spring Boot 以及手写一个 stater - 图26【20200524】深度解析 Spring Boot 以及手写一个 stater - 图27 8、最后测试用户不选择的情况下,默认使用 fastjson,圆满成功!【20200524】深度解析 Spring Boot 以及手写一个 stater - 图28【20200524】深度解析 Spring Boot 以及手写一个 stater - 图29 至此,本个需求已经成功做出来了,我力求在做的过程中将手写一个 starter 所需用到的技术都讲到,串联起来,希望大家喜欢!

后记

小伙伴们,Spring Boot 的使用中极大的简化了我们的使用,我也是14年毕业就开始用 SSM,那时候各种配置各种依赖,各种 XML 很是恶心。但是现在有了 Spring Boot 在几分钟内就可以让我们快速搭建一个项目跑起来,时代在变迁,也感谢让我们越来越便利,在便利的过程中,我们还是要对底层的原理有一点的了解,而不是浮在上面只会使用,否则一但有问题找起来也不方便。
本文带大家对 Spring Boot 底层入了个门,还有 SringApplication 实例创建,设置初始化器和监听器,以及 run 方法里干的一些事,内置 Tomcat 如何实现的,由于篇幅有限都没有讲,留给小伙伴自行研究。最后原创不易,如果觉得写得不错,请点一个赞

作者:jack_xu
链接:https://juejin.im/post/5ec7d2f5e51d45785c693a9e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。