springboot自动配置原理和自定义starter实战

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. 总结

到这里我们可以回到最开始我们提出的三个问题

  1. springboot是怎么知道啊我们需要自动初始化内置tomcat的?

    通过自动配置源码中的@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })注解可以知道,自动配置需要classpath下存在Tomcat.class。而这个class是在spring-boot-starter-web中引入的。由此可知,在我们引入spring-boot-starter-web依赖时,springboot便知道我们的需要了。就相当于你在快餐店点餐,你不需要知道它是怎么做的,你只需要提需求即可。
  2. springboot是如何进行自动配置的?

    自动配置主要通过条件注入的方式配置,满足条件则配置。
  3. 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,撒花★,°:.☆( ̄▽ ̄)/$:.°★ 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值