--- title: Spring Boot 简介 slug: springboot-brief-intro date: 2017-05-13 12:51:59 updated: 2020-03-01 18:05:31 tags: - Spring - 演讲 category: 编程 cover: /images/2019/05/2019050913525690.jpg summary: 第一次使用 Springboot 应该是15年年底,当时就被这种约定大于配置的设计惊呆了。那个时候才从上一家公司跳槽,用的是 Spring 3。 --- 第一次使用 Springboot 应该是15年年底,当时就被这种约定大于配置的设计惊呆了。那个时候才从上一家公司跳槽,用的是 Spring 3。所以每次开发新项目的时候,配置起来都让我十分痛苦,也因此喜欢上了 Springboot 的种种便利。 众观 Springboot 的发展,可以发现,其在简化开发上不断地进步。很多常见的组件框架也有了 Springboot 版本。我想,作为 Java 程序员,是时候进入 Springboot 的世界了。 这里分享一份一年前,我在公司内部分享上用的 Slide,以期对于阅读此文的你有所帮助。 ![](/images/slide/spring-boot-intro/01.jpg) 大家好,今天由我来向大家简单介绍一下 Spring Boot 相关的内容,详细的与 Spring Boot 相关的知识将后面由吴一敏同学分享。 首先自我介绍一下,我叫盛宇帆,15年年底加入 OneAPM,现在已经1年多了,目前主要负责和告警引擎相关的开发。 ![](/images/slide/spring-boot-intro/02.jpg) Spring 官方的博客介绍 Spring: **Spring** is the **“glue”** in your application。我认为 Spring Boot 就是 A glue in Spring Framework。 相信大家都经历过配置 Spring XML 的阶段,十分痛苦地去配置一个 Bean,后面 Spring 3发布之后,基本上很多配置都是通过注解加扫包去配置初始化。 我常见到的一种配置方式,就是和 Spring 框架集成部分的配置,如数据库啊,Web 模板一类的,使用的是 XML,自己项目的 Service、Dao 等类,使用注解初始化。 后面 Spring 4开始流行 @Configuation 注解的配置类初始化配置。 然而,这样子还是十分麻烦,所以才有了 Spring Boot,它给我们最直观的感受,就是 简化了配置。一言一概之,约定大于配置。 然而,仅有这些,并不能说明为何现在 Spring Boot 开始流行,说道 Spring Boot 的兴起,我想起前几天一个技术群的提问:为什么 Spring Boot 应用倾向于打 fat jar 直接启动,而传统的应用倾向于打 war 包从应用容器启动? ![](/images/slide/spring-boot-intro/03.jpg) Java 应用部署于应用容器中,其实是受到 J2EE 的影响,也算是 Java Web 有别于其他 Web 快速开发语言的一大特色。一个大大的 war 压缩包,包含了全部的依赖,代码,静态资源,模板。 在虚拟化流行之前,应用都是部署在物理机上的,为了节约成本,多 war 包部署在一个 Servlet 容器内。 但是为了部署方便,如使用的框架有漏洞、项目 jar包的升级,我们会以解压 war 包的方式去部署。或者是打一个不包含依赖的空 war 包,指定容器的加载某个目录,这样所有的war项目公用一套公共依赖,减少内存。当然缺点很明显,容易造成容器污染。 避免容器污染,多 war 部署变为多虚拟机单 war、单容器。 DevOps 流行,应用和容器不再分离,embedded servlet containers开始流行 Spring Boot 在这个阶段应运而生。于是项目部署变为 fat jar + 虚拟机 Docker的流行,开始推行不可变基础设施思想,实例(包括服务器、容器等各种软硬件)一旦创建之后便成为一种只读状态,不可对其进行任何更改。如果需要修改或升级某些实例,唯一的方式就是创建一批新的实例以替换。 基于此,我们将配置文件外置剥离,由专门的配置中心下发配置文件。 这也是我们为何要学习和使用 Spring Boot 的背景,我觉得这才是 Spring Boot 开始流行的主要原因。 ![](/images/slide/spring-boot-intro/04.jpg) 总的来说 Spring Boot 有以下几个特点。 1. 配置简化,这个我印象最深刻的就是写 MyBatis 的时候,一堆东西要配置,一般大家都会用那个 Generator 去生成。而实际上 Spring Boot 推崇 jpa,如果只是简单的 CRUD,用 Spring Boot + JPA 的方式简单到只需要几行关于数据库的配置就好了。 2. 自动配置机制,很多教程都称它为 Magic,基于项目的某些条件,自动初始化装配必要的 Bean,稍后会在后面的演示中详解。 3. Starter 本质上就是 Spring 基于 Gradle 和 Maven 这两种构建工具定义的一组依赖,一般是按照功能或者框架划分。在有了自动配置的机制下,我们只需要依赖 Starter 指定的坐标,和非常简单的属性配置即可集成我们想要的框架。 4. 嵌入的 Servlet 容器,主要是为了方便部署的。 5. 主要是 `spring-boot-starter-actuator` 和 `spring-boot-starter-remote-shell` 的使用,当然,这里还可以使用 JMX 一类的做管理,大家可以参考文档。(2017年更新 remote shell 已经废弃) ![](/images/slide/spring-boot-intro/05.jpg) 首先,我们来用 Spring Boot 写一个 Hello World吧,这个是仿照 Spring 官方的示例代码改的,使用 Groovy,所以连 import 都省了。这里主要是为了演示一个最简单的 Spring Boot 应用,通过下面的这行命令我们就能把它启动了。 ```java @RestController class GreetingRestController { @RequestMapping("/hi/{name}") def hi(@PathVariable String name) { [ greeting: "Hello, " + name + "!"] } } ``` ![](/images/slide/spring-boot-intro/06.jpg) 现在,问题来了,刚才那个项目那么简单,那个,整个项目的启动过程中,到底发生了哪些魔法呢? ![](/images/slide/spring-boot-intro/07.jpg) 我们将 Groovy 的代码翻译为 Java版本,大概会看到,一个项目想要启用 Spring Boot,关键在于 `SpringApplication` 类和 `EnableAutoConfiguration` 注解的使用。 SpringApplication 是 Spring Boot 提供的用于 Java main 方法的启动类。它的执行操作首先为: 1. Create an appropriate ApplicationContext instance (depending on your classpath) 2. Register a CommandLinePropertySource to expose command line arguments as Spring properties 3. Refresh the application context, loading all singleton beans Trigger any CommandLineRunner beans 然后 `EnableAutoConfiguration` 则为 Enable 类注解这里通过此注解,告诉 Spring Boot 开启自动装配的特性。 ![](/images/slide/spring-boot-intro/08.jpg) 除了 `EnableAutoConfiguration`,我们常常和它并列使用的还有 `ComponentScan` `Configuration` 注解。这三个注解合起来,有一个等价的注解,叫做 `SpringBootApplication`,一般在我们的项目开发中,喜欢在项目最外面的包下面创建包含 main 方法的程序启动类,然后这个类上标记为 @SpringBootApplication 这个注解,这样就等价于基于 main 方法类所在的 package 为 Spring 扫包的基础包路径,且开启自动化配置。 自动化配置的实现,不得不说 Spring Boot 本质上是通过 Conditional 类注解来实现的。 `@ConditionalOnClass` 表示对应的类在classpath目录下存在时,才会去执行注解所标示的自动配置类或者自动配置方法,与之对应的我们就@ConditionalOnMissingClass 注解,也就是找不到对应的类的时候。 `@ConditionalOnBean` 和 `@ConditionalOnMissingBean` 则同样很容易按照字面意思理解。 当然 `Conditional*` 注解不仅仅上面提到的这些,还有 `ConditionalOnExpression` 一类的。 ![](/images/slide/spring-boot-intro/09.jpg) 这个是我们从 Spring Boot 当中节选的一段代码,主要是为了演示自动化配置的具体实现。首先我们在项目配置里面标明 spring.jmx.enabled = true,ConditionalOnProperty 注解生效,然后 Spring 发现能找到 `MBeanExporter.class` 这个类,于是开始执行自动化配置的方法,因为这个时候项目中没有定义 `MBeanExporter` 这个 Bean,于是 `ConditionalOnMissingBean` 注解生效,Spring 开始读取配置属性,自动创建此 Bean 对象。 同理,任何一个自动装配的实现,基本上就是组合这些条件注解。 ![](/images/slide/spring-boot-intro/10.jpg) 当然,如果能被 Spring Boot 官方直接支持的话是最吼的,目前被支持的肯定不止上面这些,我只是简单地列举了一些常见的项目。 Spring Boot 官方之前发起过好几次投票,就是列举一些框架,然后大家投票选择哪些想要被官方支持的。上半年的时候,我还在里面看到了之前姜老师维护的 camel,然而似乎并没有被选中。MyBatis 目前虽然有 Spring Boot 版,但是是由 MyBatis 团队自行维护,至少我6月份尝试使用的时候,问题还是蛮多的。 (2017年之后的版本基本可用,主要是有了 Boot 版本的 VFS) ![](/images/slide/spring-boot-intro/11.jpg) 上面是我从 Spring Boot 1.3.6 中找到的 spring.factories 文件的截图。当然,基本上只要上面有的,都能得到不错的支持。 ![](/images/slide/spring-boot-intro/12.jpg) 前面我们简单介绍了自动配置的实现原理,基本上流程就是配置文件标明启用什么服务,然后找到对应的依赖(class),然后结合条件装配初始化 Bean。 所以 Spring 就更进一步,按照功能模块,划分出一个个 Starter 模块。以 Maven 为例,基本上我们只需要将 Spring Boot 自己的那个 POM 文件设置为 parent,然后依赖中直接依赖所需的 Starter 坐标,即可依赖所有所需的 jar 包,剩下的的东西,仅有最基础的属性值配置。 ![](/images/slide/spring-boot-intro/13.jpg) 当然 Starter 也是一把双刃剑,比如我在项目里面依赖了 `spring-boot-starter-data-jpa` 之后在 IDEA 里面看到的依赖树,简直就是 jar 包狂魔,虽然我们需要 jpa,但是不一定需要全部这些包。 ![](/images/slide/spring-boot-intro/14.jpg) 这也就引出了我使用 Starter 的时候的几个困扰。比如项目需要以 spring-boot 的 pom 为 parent,这个就比较讨厌了,尤其是我前公司,所有的项目是同一内部的 parent,这样子可以管理大家的依赖。如果要用Spring Boot 的话,就会略有麻烦。可能就需要通过依赖 Spring Boot 的 pom 的方式,并不是很优雅。 问题二是我在用 Spring Boot 时依赖 logstash 遇到的, logstash自己依赖了一个 logback-access 和那个版本的 Spring Boot 依赖的 logback 不一致,导致一直报一个 `java.lang.AssertionError` 问题三就是我最近想用 Spring Boot 去读写 Kafka,结果我们用的 Kafka 版本比较老,最后只好自己配置,特别麻烦。很多老的组件,要么你得用老的 Spring Boot,要么你就得自己配置。 ![](/images/slide/spring-boot-intro/15.jpg) 比如我们基于 YAML 定义了上图这么一段配置,最简单的方式就是 `@Value` 注解,通知这货还支持 Spring El 表达式,做一些简单的处理判断。 但是对于一个组件的配置,或者是项目自己的配置,更需要比较好的梳理,Spring Boot 便支持了所谓的 prefix 前缀的概念,我们可以把所需要的配置信息定义为一个配置类,在里面定义好必要的 Getter Setter 一类的东西。在初始化项目的时候使用 `@EnableConfigurationProperties` 注解即可实现配置参数注入到配置类里面。 当然麻烦的地方在于如果配置文件定义的层级过深,配置类会变得极其复杂。建议这种情况下,尽可能简化层级。 ![](/images/slide/spring-boot-intro/16.jpg) 配置文件的加载,其实 Spring Boot 有一个非常复杂的流程,大家好奇的话可以看 Spring Boot 文档中的定义,大概有十几种情况。但是大部分情况下,我们用不了这么多,上面是我认为应该知道并且利用的几种情况,配置加载的顺序是由上往下。 第一种情况,和 jar 包在同一目录下,一般是应用发布到生产,然后还想修改更新配置的情况。 项目 resources 目录和 resources/config 下面的配置文件,就是我们在开发的时候会选取的存放配置的位置。 当然配置文件的名称默认是 application,还可能根据你所启用的 Profile 加载不同名称的配置文件。 由于配置文件的指定在 Spring Boot 中极其灵活,(官方可能把所有的情况都考虑到了)所以大家可以自己按需选择。 ![](/images/slide/spring-boot-intro/17.jpg) 测试当然也有 Starter,我们只需要依赖 `spring-boot-starter-test`,即可轻松写测试。常见的测试注解就是上面几个 `@WebIntegrationTest` 注解相当于 `@IntegrationTest` + `@WebAppConfiguration` 注解结合使用,在 1.3 之前,主要是使用前面4个注解进行测试。 1.4 之后,我们主要使用 `SpringBootTest` 注解做测试 ![](/images/slide/spring-boot-intro/18.jpg) 最老的方式,你可能会使用 `@ContextConfiguration` 注释和 `SpringApplicationContextLoader` 的组合去写单元测试。 当然,我们可以去掉 loader 的配置,使用方法2 的 `SpringApplicationConfiguration` 注解去测试 ![](/images/slide/spring-boot-intro/19.jpg) 当想写一个集成测试的时候,可以使用 `IntegrationTest` 注解,和前面的不一样的是,前面两种方式不会初始化全部的 Bean,而 `IntegrationTest` 会和生产环境一样,完整初始化程序。但是它不会初始化 嵌入式的 Servlet 容器。 当你需要嵌入式的 Servlet 容器做一些接口的集成测试的时候,就需要使用 `WebIntegrationTest` 注解 ![](/images/slide/spring-boot-intro/20.jpg) 然而一个项目,基本上包含 `SpringApplication` 和 main 方法的类只有一个,所以在1.4 之后,我们连 App.class 都不需要给定,直接使用 `@SpringBootTest` 注解即可,更加优雅。 ![](/images/slide/spring-boot-intro/21.jpg) 这个就是我基于 Spring Boot 1.3 写的一个集成测试,当然它使用的是我们前面说的方法3。 ![](/images/slide/spring-boot-intro/22.jpg) 前面我们其实已经说到了 Spring Boot 的 `Profile` 可以让我们区分不同环境下加载的配置文件。比如开发环境,测试和线上,很多值都可以实现定制,而不需要重新打包项目。 使用 Profile 的第二个场景就是不同的 Profile 需要初始化不同的bean,比如以 DataSource 为例,测试的时候,因为测试环境不一样,我们更期望 DataSource 能用 H2一类的嵌入式数据库模拟。开发和生产环境,就需要初始化一个 MySQL 的 DataSource。当然我说的这个场景不需要我们专门去配置,因为 Spring Boot 已经替我们考虑到了这种情况,在需要 DataSource,但是没有这个 Bean,切依赖了 H2的 Driver 的时候,Spring 会自动创建一个 H2 的 DataSource。 还有一个我使用 Profile的场景就是 Swagger,它十分好用,尤其是开发的时候能基于注解自动生成 API Doc,和测试页面,然而,会存在的问题就是它有漏洞,我只希望在开发的时候启用 Swagger,这个时候就可以利用 Profile 来实现。 ![](/images/slide/spring-boot-intro/23.jpg) Profile 可以启用一个或者多个,然而,也会导致一些问题,比如我们没有指定 Profile 的时候怎么办,或者我们有 profile 名为 mysql、cassadra。它们是相互 block 的,如何检查校验,避免冲突的 profile 同时启用呢? ![](/images/slide/spring-boot-intro/24.jpg) 这个是添加默认 Profile 的方式,原来我是尝试在 application 配置文件里面设置,但是不生效,最后我只好手动编码实现。 ![](/images/slide/spring-boot-intro/25.jpg) 这个方法是和 main 方法同级的一个方法,需要 Autowired Spring 的 Environment 接口,然后获取 Profile 的配置,即可自行实现判断逻辑。(期待后面 Profile 能更加完善,实现 Block 一类的属性) ![](/images/slide/spring-boot-intro/26.jpg) ![](/images/slide/spring-boot-intro/27.jpg) ![](/images/slide/spring-boot-intro/28.jpg) ![](/images/slide/spring-boot-intro/29.jpg) Spring Boot 官方推崇的部署方式是 jar,原因我们前面也分析过了,但是也会存在打包为 war 去部署的需求。这里我们只需要在和 Application.class 同级的路径下继承 SpringBootServletInitializer 类去定义一下 SpringApplicationBuilder 的配置即可,还是很轻松的。我这个截图的示例里面用了前面设置默认 Profile 的方法,重用了一下代码。 有了这么一个类之后,我们就可以在 pom 里面设置项目打包为 war,它既能 java –jar去执行这个 war,也能直接丢到 Tomcat 一类的容器里面运行。 我们在 17年的实践中发现,很多时候,非 Fatjar 也有一定的意义,于是有了下属的打包启动实践,大家可以去参考。 https://gist.github.com/syhily/c66310c150653e8f92b9fa6693df8207 ![](/images/slide/spring-boot-intro/30.jpg) 如果想要快速创建一个 Spring Boot 项目开发,有且不仅有上述几种方式。 ![](/images/slide/spring-boot-intro/31.jpg) ![](/images/slide/spring-boot-intro/32.jpg) ![](/images/slide/spring-boot-intro/33.jpg) 你可以点击 [这里](https://cat.yufan.me/uploads/springboot-intro.pptx) 下载到本地浏览。