SpringBoot自定义starter

一、简介

SpringBoot由众多的starter组成,这些starter也被称为是场景启动器,在工程中引入特定的starter再进行少量的配置就可以使用其提供的相应的功能了,SpringBoot在不断的维护和扩展不同场景的starter给使用者提供更完善的功能,我们也可以创建自定义的starter制定我们自己的特定场景。

二、SpringBoot中的starter

  1. 我们查看SpringBoot提供的starter可以发现所有的starter包下都没有任何代码

    starter
  2. 我们找到spring-boot-starter-data-redis.jar包的pom.xml查看一下内容

    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
    <?xml version="1.0" encoding="UTF-8"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starters</artifactId>
    <version>2.2.6.RELEASE</version>
    </parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.2.6.RELEASE</version>
    <name>Spring Boot Data Redis Starter</name>

    <!-- 省略一部分 -->

    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.2.6.RELEASE</version>
    <scope>compile</scope>
    </dependency>
    <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.2.6.RELEASE</version>
    <scope>compile</scope>
    <exclusions>
    <exclusion>
    <artifactId>jcl-over-slf4j</artifactId>
    <groupId>org.slf4j</groupId>
    </exclusion>
    </exclusions>
    </dependency>
    <dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.2.2.RELEASE</version>
    <scope>compile</scope>
    </dependency>
    </dependencies>
    </project>

    我们看到这个starter继承自spring-boot-starters包,并且所有的starter都引入了spring-boot-starter包,包中还可以根据需要选择性的引入其他的jar包。

  3. 进入到spring-boot-starter包中,可以找到它引入了spring-boot-autoconfigure包,这是我们完成自动配置的关键

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.2.6.RELEASE</version>
    <scope>compile</scope>
    </dependency>

    查看spring-boot-autoconfigure.jar的目录结构发现包含了各种starter的配置信息。

    • 标1:需要自动配置的类声明文件
    • 标2:配置文件中自动提示的元属性数据声明文件
    • 标3:自动配置控制类
    • 标4:场景属性类
    autoconfigure

以上就是一个starter的关键所在了,总结下来就是:

  • 启动器starter只是用来做依赖管理的,其不应该包含任何代码和配置,需要引入autoconfigure包
  • 自动装配autoconfigure包,需要包含我们需要让SpringBoot自动装配的模块,以及资源配置信息,总的来说包括:spring.factories、spring-configuration-metadata.json、XxxAutoConfiguration、XxxProperties
  • 使用的时候只需要引入启动器starter就可以实现自动配置

三、自定义starter命名规范

  • 官方命名规范
    • 规则:spring-boot-starter-模块名
    • 举例:spring-boot-starter-data-redis、spring-boot-starter-web
  • 自定义命名规范
    • 规则:模块名-spring-boot-starter
    • 举例:cc-spring-boot-starter

四、自定义starter demo

根据上面的描述,我们需要创建两个项目:cc-spring-boot-autoconfigure和cc-spring-boot-starter

  1. 创建cc-spring-boot-autoconfigure工程

    需要注意的是在工程创建完成之后,要删除启动类、application.properties和test文件夹

    • pom.xml和控制类

      • pom.xml

        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
        <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starters</artifactId>
        <version>2.2.6.RELEASE</version>
        </parent>
        <groupId>cc.lu</groupId>
        <artifactId>cc-spring-boot-autoconfigure</artifactId>
        <version>1.0</version>

        <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        </properties>

        <dependencies>
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        </dependency>
        </dependencies>

        </project>
      • CcProperties

        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
        package cc.lu.autoconfigure.wt;

        import org.springframework.boot.context.properties.ConfigurationProperties;

        @ConfigurationProperties(prefix = "cc.config")
        public class CcProperties {

        private String name = "cc";

        private Integer age = 3;

        private String birthday;

        public String getName() {
        return name;
        }

        public void setName(String name) {
        this.name = name;
        }

        public Integer getAge() {
        return age;
        }

        public void setAge(Integer age) {
        this.age = age;
        }

        public String getBirthday() {
        return birthday;
        }

        public void setBirthday(String birthday) {
        this.birthday = birthday;
        }

        @Override
        public String toString() {
        return "CcProperties{" + "name='" + name + '\'' + ", age=" + age + ", birthday='" + birthday + '\'' + '}';
        }
        }

        类的有些属性被设置了值,这些值就是当配置文件未配置属性时使用的默认值,这就是参数配置设置默认值的一种方式。还有一种,继续往下看

      • CcService

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        package cc.lu.autoconfigure.wt;

        public class CcService {

        private CcProperties ccProperties;

        public void setCcProperties(CcProperties ccProperties) {
        this.ccProperties = ccProperties;
        }

        public String info() {
        return ccProperties.toString();
        }

        }

        一个普通的类,没啥可说的,继续往下

      • CcAutoConfiguration

        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
        package cc.lu.autoconfigure.wt;

        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
        import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
        import org.springframework.boot.context.properties.EnableConfigurationProperties;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;

        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass({ CcService.class })
        @EnableConfigurationProperties({ CcProperties.class })
        public class CcAutoConfiguration {

        @Autowired
        private CcProperties ccProperties;

        @Bean
        @ConditionalOnMissingBean({ CcService.class })
        public CcService ccService() {
        CcService ccService = new CcService();
        // 打个日志查看方法是否被调用
        System.out.println("------auto register!------");
        ccService.setCcProperties(ccProperties);
        return ccService;
        }

        }

        CcAutoConfiguration的声明上使用了@ConditionalOnClass和@EnableConfigurationProperties做加载控制,然后在类中创建了CcService的实例,并且使用@ConditionalOnMissingBean({ CcService.class })来控制创建条件。

    • spring.factories和spring-configuration-metadata.json配置

      • spring.factories

        1
        2
        org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
        cc.lu.autoconfigurer.wt.CcAutoConfiguration

        将CcAutoConfiguration放给EnableAutoConfiguration,告知SpringBoot启动的时候进行检测加载。

      • spring-configuration-metadata.json元属性数据配置

        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
        {
        "groups": [
        {
        "name": "cc.config",
        "type": "cc.lu.autoconfigure.wt.CcProperties",
        "sourceType": "cc.lu.autoconfigure.wt.CcProperties"
        }
        ],
        "properties": [
        {
        "name": "cc.config.age",
        "type": "java.lang.Integer",
        "sourceType": "cc.lu.autoconfigure.wt.CcProperties",
        "defaultValue": 3
        },
        {
        "name": "cc.config.birthday",
        "type": "java.lang.String",
        "sourceType": "cc.lu.autoconfigure.wt.CcProperties"
        },
        {
        "name": "cc.config.name",
        "type": "java.lang.String",
        "sourceType": "cc.lu.autoconfigure.wt.CcProperties",
        "defaultValue": "cc"
        }
        ],
        "hints": []
        }

        这个文件的目的是在properties或yml文件中配置自定义属性的时候,可以自动提示,例如:

        image-20200423023445975

        这个文件看上去好像非常复杂,尤其是当我们有几十个自定义配置属性的时候,难道要一个个属性的去写吗?答案是当然不用,可以通过引入一个jar包来在打包的时候自动生成:

        1
        2
        3
        4
        5
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
        </dependency>

        引入之后,执行完命令mvn clean package之后,到target/classes/META-INF目录下查看自动生成的文件

        image-20200423024104706

    至此,我们的cc-spring-boot-autoconfigure就创建完成了,回瞄一眼,是不是和官方的autoconfigure包含的文件一致了。

  2. 创建cc-spring-boot-starter工程

    一个很普通的maven工程,因为starter中一般不包含任何代码,仅仅作为包的依赖管理工程,所以创建完成之后依然要删除启动类、application.properties和test文件夹,它只有一个pom.xml

    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
    <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cc.lu</groupId>
    <artifactId>cc-spring-boot-starter</artifactId>
    <version>1.0</version>
    <name>cc-spring-boot-starter</name>

    <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    </properties>

    <dependencies>
    <dependency>
    <groupId>cc.lu</groupId>
    <artifactId>cc-spring-boot-autoconfigure</artifactId>
    <version>1.0</version>
    </dependency>

    </dependencies>

    </project>
  3. 创建cc-starter-demo测试工程

    创建一个常规的SpringBoot工程,引入我们自定义的starter,并设置相关的配置信息,就可以使用了。

    • pom.xml

      1
      2
      3
      4
      5
      <dependency>
      <groupId>cc.lu</groupId>
      <artifactId>cc-spring-boot-starter</artifactId>
      <version>1.0</version>
      </dependency>
    • application.properties

      修改birthday和name两个属性,age使用默认值,待会看效果

      1
      2
      cc.config.birthday=2017-03-09
      cc.config.name=yc
    • 创建一个Controller

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      package cc.lu.starter.demo.controller;

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;

      import cc.lu.autoconfigurer.wt.CcService;

      @RestController
      public class PersonController {

      @Autowired
      private CcService ccService;

      @GetMapping("/info")
      public void info() {
      System.out.println(ccService.info());
      }

      }

五、测试

1. 情况一
  • 启动cc-starter-demo工程,查看启动日志,会发现我们在CcAutoConfiguration中打印的字符串------auto register!------出现在了控制台,说明CcService的实例创建是在CcAutoConfiguration中完成的。

    image-20200423030101381
  • 访问http://127.0.0.1:8080/info,查看控制台的日志,看到我们在application.properties文件中配置的值生效了,也就是我们的starter写的正常。

    image-20200423030335580
2. 情况二
  • 在启动类中重新定义CcService的实例创建方法

    1
    2
    3
    4
    5
    6
    7
    8
    @Bean
    public CcService ccService(CcProperties ccProperties) {
    CcService ccService = new CcService();
    // 打个日志查看方法是否被调用
    System.out.println("====client register====");
    ccService.setCcProperties(ccProperties);
    return ccService;
    }
  • 启动工程,查看控制台是打印了字符串====client register====还是------auto register!------

    image-20200423030703200

    控制台打印的是我们demo工程里创建CcService实例方法内的字符串,而CcAutoConfiguration内的打印语句未执行,这是因为我们在CcAutoConfiguration类中的方法加了@ConditionalOnMissingBean({ CcService.class })来控制实例的创建。(划知识点,约定大于配置)

六、总结

通过自定义starter分析一下其工作原理:

  1. SpringBoot在启动时扫描项目依赖的全部jar包,并寻找META-INF/spring.factories文件
  2. 根据META-INF/spring.factories加载符合条件的AutoConfigure
  3. 根据@Conditional注解条件进行自动配置,并将Bean注入到Spring Context中。