1. 概述
在熟悉了spring繁琐配置的折磨之后,终于迎来了springboot。springboot极大的简化了原来spring的许多配置,提供了许多已经精心装配和测试好的套餐可供使用。 在开始本章之前,请回忆一下我们的第一个spring boot 的hello world程序,你做了哪些操作
1.1从hello world开始
开始我们首先要创建一个springboot项目吧。相信这对于大多数胖友还是小菜一碟的,这里就不详述了。最后你成功的创建了springboot项目。
其中pom如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.3.1.RELEASE</version>
</parent>
<groupId>org.example</groupId>
<artifactId>springbootDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
DemoApplication如下:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class,args);
}
}
TestController如下:
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello world";
}
}
至此,你可以在浏览器中输入http://localhost:8080/test/hello,就完成了你的第一个“hello world”程序
1.2 三个问题
回到最初提出的问题,我们的入门程序做了哪些操作
- 首先我们会有一个带@SpringBootApplication注解的Application,该类负责我们项目的启动
- 其次,我们引入了spring-boot-starter-web依赖以支持web应用
- 然后,我们添加了一个controller
以前我们在spring中那一大堆配置瞬间别springboot缩减成了这简简单单的三步。(不经让人想起之前同事说的,感觉编程越来越傻瓜式了,是不是以后程序员可以失业了,想来我们还真是被自己“聪明”死的)
这简简单单的三步,不再需要我们配置web.xml 不需要配置spring-mvc.xml。这些操作都被springboot封装到了“小黑盒”中。所以随之而来的问题是搞清楚它如何做到这一切的。
运行我们的demo程序的时候,你会发现springboot会自动配置初始化一个tomcat容器。我们就以此为例讲解springboot的自动配置
到这里我猜你可能会有以下这几个问题:
- springboot是怎么知道啊我们需要自动初始化内置tomcat的?即我们是怎么告诉springboot我们需要某种配置的
- springboot是是如何进行自动配置的?即自动配置到底做了哪些事儿
- springboot如何发现这些自动配置的?springboot有那么多自动配置,它又怎么加载这些自动配置的
现在我们就带着这三个问题进行一次短暂的源码之旅
2. 自动配置大揭秘
首先我们需要下载springboot的源码,不知道怎么操作的胖友请查看Spring Boot源码——源码阅读环境搭建
2.1 各种不同的套餐(starter)
springboot有许多不同的starter,这些starter就如同快餐店的套餐,这些套餐已经给你搭配好,可能是豆浆油条,可能是咖啡汉堡,你只需要从中选择满足你需要的即可。在springboot中有一个spring-boot-starter的项目
可以看到该项目下包含了各种各样的starter。自然也包含了我们demo中引入的spring-boot-starter-web。我们看看它的pom文件
可以看到每个starter项目都只有一个pom文件,这些starter不做任何逻辑代码处理。只是用来将对应功能模块的包整理在一起,然后通过maven依赖的继承,当我们引入这些starter后就自然也有这些依赖了。 每个starter中都会引入spring-boot-starter项目,而这个项目中会引入spring-boot-autoconfigure项目,从名字上看我们也知道,springboot的自动配置就靠它了。
2.2 条件配置和属性配置
spring-boot-autoconfigure项目中包含springboot所有的内置自动配置,其中tomcat容器的自动配置在org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
类中。
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
/**
* 配置tomcat
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
/**
* 配置jetty
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Server.class, Loader.class, WebAppContext.class })
public static class JettyWebServerFactoryCustomizerConfiguration {
@Bean
public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new JettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
//省略其他代码……
}
以上是内置服务容器自动配置原代码:
- @Configuration:spring中提供的java配置注解,以此标记该类是一个配置类,此类中所有@Bean标记的方法返回的对象将被注入到spring容器中
- @ConditionalOnWebApplication:springboot提供的注解,判断是否是web项目。spring中提供了@Conditional注解,用以注入满足条件的对象到容器中
- @EnableConfigurationProperties:springboot提供的注解,用以指明属性配置类,其指明的配置类可以在resource的application属性文件中配置。
- @ConditionalOnClass:当作为参数的类存在则注入对应的对象
可以看到,tomcat容器的自动配置还是比较简单的,通过spring提供的java条件配置功能,当classpath下存在Tomcat.class, UpgradeProtocol.class的时候就会去new一个tomcat容器。而tomcat正是从我们的starter中引入的
我们再来看一看ServerProperties.class,其中指明tomcat容器初始化的一些属性,其源码如下:
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
/**
* Server HTTP port.
*/
private Integer port;
/**
* Network address to which the server should bind.
*/
private InetAddress address;
//省略其他代码……
}
这个文件中的代码还蛮多的,不过无非是一些属性项和默认值处理之类的。我们看到了我们熟悉的port属性,是不是有种豁然开朗的感觉(_)
- @ConfigurationProperties:标识该类是一个属性配置文件,其中prefix表示的是该类中所有配置项在配置时的前缀。可以看到这里的前缀是server,所以我们可以在application.yaml文件中如下更改tomcat的端口
server:
port: 8082
2.3 springboot的spi机制
什么是spi机制?spi全程为service provider interface,这是一种服务发现机制。
- 系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
- java中的spi约定,当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了。
简单点来说spi机制是一种解耦服务使用方(接口定义模块)和服务提供方(各种不同的服务提供模块)的机制。
如果读完以上解释还不甚了解的朋友可以去看一看深入理解SPI机制
在springboot中,spring-boot-autoconfigure中的MATE-INF下存在一个spring.factories的文件。其中包含了所有springboot启动时需要加载的类。(注意java的类加载机制是,在使用某个类时才会加载到jvm中。所以这里需要主动进行类的加载)
springboot在启动执行SpringApplication.run()方法的时候会扫描所有jar包中MATE-INF下的spring.factories文件,并将其中声明的类加载到jvm中。
3. 总结
到这里我们可以回到最开始我们提出的三个问题
-
springboot是怎么知道啊我们需要自动初始化内置tomcat的?
通过自动配置源码中的@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })注解可以知道,自动配置需要classpath下存在Tomcat.class。而这个class是在spring-boot-starter-web中引入的。由此可知,在我们引入spring-boot-starter-web依赖时,springboot便知道我们的需要了。就相当于你在快餐店点餐,你不需要知道它是怎么做的,你只需要提需求即可。 -
springboot是如何进行自动配置的?
自动配置主要通过条件注入的方式配置,满足条件则配置。 -
springboot如何发现这些自动配置的 ?
springboot通过SpringFactoriesLoader扫描所有jar包中MATE-INF下的spring.factories文件,并将其中声明的类加载到jvm中
4. 秀一波,一个自定义starter实现swagger自动配置
不知道胖友们是否使用过swagger。swagger是一套api,用于我们生成规范的且能自动更新的api文档。有了这个利器,只需要在写接口的时候添加几个注解,就不用手动的去写api文档了,且更新接口的时候也不需要担心api文档更新不及时引起内部矛盾了(以前我们都是手动维护api文档,内部矛盾时有发生,哈哈哈)
4.1 swagger常用注解
@Api 注解的常用属性,如下:
- tags 属性:用于控制 API 所属的标签列表。[] 数组,可以填写多个。
- 可以在一个 Controller 上的 @Api 的 tags 属性,设置多个标签,那么这个 Controller 下的 API 接口,就会出现在这两个标签中。
- 如果在多个 Controller 上的 @Api 的 tags 属性,设置一个标签,那么这些 Controller 下的 API 接口,仅会出现在这一个标签中。
- 本质上,tags 就是为了分组 API 接口,和 Controller 本质上是一个目的。所以绝大数场景下,我们只会给一个 Controller 一个唯一的标签。例如说,UserController 的 tags 设置为 "用户 API 接口" 。
@ApiOperation 注解用以描述接口,常用属性,如下:
- value 属性:API 操作名。
- notes 属性:API 操作的描述。
@ApiImplicitParam 添加在 Controller 方法上,声明每个请求参数的信息。注解的常用属性,如下:
- name 属性:参数名。
- value 属性:参数的简要说明。
- required 属性:是否为必传参数。默认为 false 。
- dataType 属性:数据类型,通过字符串 String 定义。
- dataTypeClass 属性:数据类型,通过 dataTypeClass 定义。在设置了 dataTypeClass 属性的情况下,会覆盖 dataType 属性。推荐采用这个方式。
- paramType 属性:参数所在位置的类型。有如下 5 种方式:
- path" 值:对应 SpringMVC 的 @PathVariable 注解。
- 【默认值】"query" 值:对应 SpringMVC 的 @PathVariable 注解。
- "body" 值:对应 SpringMVC 的 @RequestBody 注解。
- "header" 值:对应 SpringMVC 的 @RequestHeader 注解。
- "form" 值:Form 表单提交,对应 SpringMVC 的 @PathVariable 注解。
- 绝大多数情况下,使用 "query" 值这个类型即可。example 属性:参数值的简单示例。examples 属性:参数值的复杂示例,使用 @Example 注解。
@ApiModel 添加在 POJO 类,声明 POJO 类的信息注解。常用属性,如下:
- value 属性:Model 名字。
- description 属性:Model 描述。
@ApiModelProperty 添加在 Model 类的成员变量上,声明每个成员变量的信息。注解的常用属性,如下:
- value 属性:属性的描述。
- dataType 属性:和 @ApiImplicitParam 注解的 dataType 属性一致。不过因为 @ApiModelProperty 是添加在成员变量上,可以自动获得成员变量的类型。
- required 属性:和 @ApiImplicitParam 注解的 required 属性一致。
- example 属性:@ApiImplicitParam 注解的 example 属性一致。
4.2 未封装成starter前,springboot中如何使用swagger
先来看看如果未将swagger封装成自动配置前该怎么使用,创建一个项目springboot-swagger-base,其项目结构如下:
首先引入相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--引入swagger-ui 之后生成的api文档可以通过访问http://localhost:8080/swagger-ui.html查看-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
可以看到,因为springboot没有集成swagger,所以需要单独引入swagger的包。注意看注释
要想在springboot中启用swagger,我们还需要创建一个config,注入一个文档对象。所以我们创建一个SwaggerConfig
@Configuration
//开启swagger
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
Docket docket=new Docket(DocumentationType.SWAGGER_2)
//生成一个api的说明,最后会展现在生成的api文档的头部,相当于一个描述信息
.apiInfo(this.apiInfo())
//扫描learn.swagger.controller包下的api注释
.select()
.apis(RequestHandlerSelectors.basePackage("learn.swagger.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
private ApiInfo apiInfo(){
//文档的名称,描述,联系人等
return new ApiInfoBuilder()
.title("测试api文档")
.description("这是一个swagger的测试文档")
.contact(new Contact("yuanxu","","yx.hero@qq.com"))
.build();
}
}
其中加上@EnableSwagger2标记项目启用 Swagger API 接口文档。然后创建一个Docket对象注入spring的上下文中。Docket是swagger的一个文档对象,包含着一些基础的描述信息。
创建一个SwaggerController:
@RestController
@RequestMapping("/users")
@Api(tags = "用户相关接口")
public class SwaggerController {
@GetMapping("/list")
@ApiOperation(value = "用户信息列表")
public List<UserVo> list() {
// 查询列表
List<UserVo> result = new ArrayList<>();
result.add(new UserVo().setId(1).setUserName("测试1"));
result.add(new UserVo().setId(2).setUserName("测试2"));
result.add(new UserVo().setId(3).setUserName("测试3"));
// 返回列表
return result;
}
@GetMapping("/get")
@ApiOperation(value = "获取id对应用户")
@ApiImplicitParam(value = "用户id",name = "id",dataTypeClass = Integer.class,required = true)
public UserVo get(@RequestParam("id") Integer id) {
// 查询并返回用户
return new UserVo().setId(id).setUserName(UUID.randomUUID().toString());
}
@PostMapping("add")
@ApiOperation(value = "添加用户")
public Integer add(UserDTO addDTO) {
// 插入用户记录,返回编号
Integer returnId = UUID.randomUUID().hashCode();
// 返回用户编号
return returnId;
}
@PostMapping("/update")
@ApiOperation(value = "更新用户")
public Boolean update(UserDTO updateDTO) {
// 更新用户记录
Boolean success = true;
// 返回更新是否成功
return success;
}
@PostMapping("/delete")
@ApiOperation(value = "删除用户")
@ApiImplicitParam(value = "用户id",name = "id",dataTypeClass = Integer.class,required = true)
public Boolean delete(@RequestParam("id") Integer id) {
// 删除用户记录
Boolean success = false;
// 返回是否更新成功
return success;
}
}
UserVo:
@Data
@Accessors(chain = true)
@ApiModel("用户请求对象")
public class UserVo {
@ApiModelProperty(value = "用户id",required = true)
private Integer id;
@ApiModelProperty(value = "用户名称",required = true,example = "yuanxu",notes = "用户名称")
private String userName;
}
在浏览器中输入http://localhost:8080/swagger-ui.html查看生成的api文档
4.3 将swagger封装成starter,支持自动配置
我们创建三个项目
项目说明:
- springboot-swagger-base : 用于测试,其中包含我们上一部分提到的controller,UserVo等
- spring-boot-starter-swagger : 自己封装的starter包,该包用于封装swagger需要引入的依赖,之后只需要在springboot-swager-base中引入这个项目,则具有了swagger的功能
- springboot-swagger-autoconfig : 封装swagger自动配置功能。主要包括之前提到的swaggerConfig中的内容
4.3.1 springboot-swagger-autoconfig
首先我们先看一下springboot-swagger-autoconfig项目,其项目结构如下
首先我们引入依赖
<dependencies>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<!--这里的版本号在parent中定义,相信胖友应该懂的-->
<version>${swagger.version}</version>
</dependency>
</dependencies>
然后我们需要创建一个自动配置类SwaggerAutoConfig
@Configuration
//@ConditionalOnLocalSwagger
@ConditionalOnWebApplication
@EnableSwagger2
@EnableConfigurationProperties(SwaggerProperties.class)
public class SwaggerAutoConfig {
@Bean
@ConditionalOnClass(Docket.class)
public Docket createRestApi(SwaggerProperties swaggerProperties){
Assert.isTrue(swaggerProperties!=null,"swagger初始化失败,请在application.yaml中配置相关项");
System.out.println("swagger开始初始化……");
Docket docket=new Docket(DocumentationType.SWAGGER_2)
.apiInfo(this.apiInfo(swaggerProperties))
.select()
.apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
.paths(PathSelectors.any())
.build();
return docket;
}
private ApiInfo apiInfo(SwaggerProperties swaggerProperties){
Contact contact=new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(),swaggerProperties.getContact().getEmail());
return new ApiInfoBuilder()
.title(swaggerProperties.getTitle())
.description(swaggerProperties.getDescription())
.contact(contact)
.build();
}
}
基本上和我们之前的SwaggerConfig一样,只是我们加了一些注解:
- @Configuration:说明这是一个配置类
- @ConditionalOnWebApplication:该配置类要发生作用必须时web应用,这是springboot提供的注解,其中也是采用@Conditional条件注入实现
- @EnableSwagger2:开启swagger
- @EnableConfigurationProperties:之前提到过,该注解用于指明自动配置的属性,其中你可以定义一些默认值。如果你想对其默认值进行修改也可以通过在application.yaml中修改对应值
- @ConditionalOnClass(Docket.class):在classpath中存在Docket时才创建Docket对象
@ConfigurationProperties(prefix = "local.swagger",ignoreInvalidFields = true)
public class SwaggerProperties {
private String basePackage;
private String title;
private String description;
private SwaggerProperties.Contact contact;
public static class Contact{
private String name;
private String url;
private String email;
public String getName() {
return name==null?"":name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url==null?"":url;
}
public void setUrl(String url) {
this.url = url;
}
public String getEmail() {
return email==null?"":email;
}
public void setEmail(String email) {
this.email = email;
}
}
public String getBasePackage() {
return basePackage==null?"":basePackage;
}
public void setBasePackage(String basePackage) {
this.basePackage = basePackage;
}
public String getTitle() {
return title==null?"":title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description==null?"":description;
}
public void setDescription(String description) {
this.description = description;
}
public Contact getContact() {
if(contact==null){
return new Contact();
}
return contact;
}
public void setContact(Contact contact) {
this.contact = contact;
}
}
SwaggerProperties中我们定义了swaagger扫描的包路径,title,描述等内容,所有属性的前缀均为local.swagger
做好以上这些准备之后,我们还有最后一步。请旁友们好好回想3秒钟,我们还差哪一步?
1
2
3
好了。为了让springboot找到我们自定义的自动配置,我们还需要对MATE-INF 下的spring.factories进行改造。首先创建一个spring.factories,加上如下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=learn.swagger.config.SwaggerAutoConfig
其实就是加上我们的自动配置类。觉不觉得很神奇,通过这种方式springboot实现了可插拔。以后我们只要自己定义的自动配置只需要放在里面springboot就自动知道了。
4.3.2 spring-boot-starter-swagger
这个类比较简单只包含一个pom文件,目的只是为了将swagger需要的jar包整理在一起
<dependencies>
<!--引入swagger-ui 之后生成的api文档可以通过访问http://localhost:8080/swagger-ui.html查看-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springboot.learn</groupId>
<artifactId>springboot-swagger-autoconfig</artifactId>
<version>
${project.version}
</version>
</dependency>
</dependencies>
因为我们已经在springboot-swagger-autoconfig引入了swagger2这个包所以在这里就不在需要依赖这个包了
4.3.3 springboot-swagger-base
我们需要将原来的这个项目改造下。首先pom文件中去掉swagger的依赖,加入我们封装好的spring-boot-starter-swagger
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springboot.learn</groupId>
<artifactId>spring-boot-starter-swagger</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
原来的SwaggerConfig可以注释掉了,同时去掉SwaggerApplication上的@EnableLocalSwagger
最后我们需要在application.yaml中配置swagger的相关属性
server:
port: 8080
local:
swagger:
basePackage: "learn.swagger.controller"
title: "测试api文档"
description: "这是一个swagger的测试文档"
contact:
name: "yuanxu"
url: ""
email: "yx.hero@qq.com"
由此我们便完成了swagger自动配置的封装。准备好了吗?按住你的小心脏,测试一波
可看到初始化成功了,然后访问看看
结语
好了,花了一天半终于把这篇文章写完了。写完了后才发现,写一篇文章竟然这么困难,不过致力要做技术圈大神的蜗牛,蜗牛一定会坚持下去的。fighting,撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。