实现自定义spring-boot-starter

Why?(为什么有这篇文章)

记得当我刚开始接触springboot的时候,官网的Demo让我影响深刻。我只需要在pom文件里面添加web相关的依赖,我就自然而然地获得了以前用Springmvc需要手动配置地一堆bean的声明和依赖关系。之后随着参加的面试多了,面试官经常问到的两个问题就是:“你了解SpringAutoconfigure的原理吗?你有自己动手写过自己的starter吗?”于是乎,我就觉得这东西是的好好了解一下,就有了这篇文章。

What?(spring-boot-starter是个什么东西,解决了什么问题)

starter在英语中一般用在点菜的时候,叫做开胃小菜,而后面的主菜叫main course. 所以这也是很形象的一种比喻了,把某个部件比喻成小菜,而整个工程就是我们的main course. 那么这个spring-boot-starter的作用就是让你在构建main course前,把一些小组件(starter)很方便地整合进来。
我们可以在github上找到很多地starter,比如spring-boot-web-starter, spring-boot-rocketmq-starter… 这些starter引入之后,我们就能根据starter里面决定加载bean地条件来很方便地配置我们相关地组件。

How? (如何写一个自己地spring-boot-starter)

整体步骤

  • 新建maven项目
  • 引入自动配置依赖
  • 编写实现类
  • 编写配置类
  • 写入spring.factories
  • mvn install 生成jar包
  • 在项目中使用刚刚的starter

步骤详解

新建maven项目 略

引入自动配置依赖

<?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>

    <groupId>cn.kobelee</groupId>
    <artifactId>test-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>1.5.13.RELEASE</version>
        </dependency>
    </dependencies>

</project>

编写实现类

这里我写一个简单地实现类,这个实现类就一个方法。传入一个字符串,把这个字符串加上yml中配置的某个项,然后返回。注意这里的构造方法是需要传入一个字符串str, 并且赋值给configStr.

package cn.kobelee.util.component;

public class SimpleStringConcater {
    String configStr;

    public String concat(String str){
        return str + configStr;
    }

    public SimpleStringConcater(String str){
        configStr = str;
    }
}

编写配置类

上面的代码就传入一个字符串str然后和configStr拼接后返回。那么configStr怎么来呢?是的,从配置文件读取。那么既然要从配置文件读取,我就得写一个ConfigurationProperites的类。

package cn.kobelee.util.config;

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

@ConfigurationProperties("kobelee.util")
public class SimpleStringConcaterProperties {
    private String configStr;

    public String getConfigStr() {
        return configStr;
    }

    public void setConfigStr(String configStr) {
        this.configStr = configStr;
    }
}

上面这段代码其实就只是声明了一个configStr的属性,这个属性会从配置文件的kobelee.util.configStr读取(我们也可以用@Value注解进行配置)。这一段配置其实不是必须的,但是我们写starter的目的就是为了方便我们在使用的时候,只需要在配置文件里面写入配置信息,就可以根据配置信息自动实例化bean.比如我们可以写一个数据库连接相关的starter,那么可以把数据库连接的信息放入配置文件,用一个这样的配置类进行载入。

到目前为止,我们的实现已经写好了实现类和配置文件类。但是,还有两个问题:
1. 这个SimpleStringConcaterProperties.configStr还是没有注入到SimpleStringConcater.configStr中啊。
2. 这个bean到底要什么情况下进行实例化还没有相关代码指定。

于是,我们还需要一个autoconfigure的类。

package cn.kobelee.util.config;

import cn.kobelee.util.component.SimpleStringConcater;
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.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//这是一个Configuration类
@Configuration
//只有当classpath下存在SimpleStringConcater.class这个类的时候,才实例化这个Configuration bean
@ConditionalOnClass(SimpleStringConcater.class)
//为SimpleStringConcaterProperties.class启动配置
@EnableConfigurationProperties(SimpleStringConcaterProperties.class)
public class SimpleStringConcaterAutoconfigure {

    @Autowired
    private SimpleStringConcaterProperties properties;

    //声明这是一个bean
    @Bean
    //当容器里缺失这个bean的时候才实例化这个bean
    @ConditionalOnMissingBean
    //当配置文件节点下,有一个kobelee.util.enabled有一个true的值的后,才实例化这个bean
    @ConditionalOnProperty(prefix = "kobelee.util", value = "enabled", havingValue = "true")
    public SimpleStringConcater simpleStringConcater(){
        return new SimpleStringConcater(properties.getConfigStr());
    }

}

代码逻辑很简单,就是返回一个SimpleStringConcater的bean,但是需要有条件。我们可以看到simpleStringConcater()方法上面两个注解:
@ConditionalOnMissingBean 和 @ConditionalOnProperty
和Conditional有关的注解,在官网上我们可以看到有如下这些:

Class condition:
    @ConditionalOnClass classpath中有某个类才实例化此bean
    @ConditionalOnMissingClass classpath中
Bean condition:
    @ConditionalOnBean 有了某个bean才实例化此bean
    @ConditionalOnMissingBean 缺失某个类型的bean才实例化此bean
Property condition:
    @ConditionalOnProperty 配置文件满足某些条件才实例化
Resource condition:
    @ConditionalOnResource 有某个资源才实例化(/home/test.txt表明需要home路径下有个test.txt)
Web Application Conditions:
    @ConditionalOnWebApplication 是web应用才实例化
    @ConditionalOnNotWebApplication 不是web应用才实例化
SpEL Expression Conditions:
    @ConditionalOnExpression:根据SpEL表达式的返回结果来决定是否实例化

参考官网文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/html/spring-boot-features.html#boot-features-condition-annotations

编写spring.factories

在resources/META-INF下新建

org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.kobelee.util.config.SimpleStringConcaterAutoconfigure

这个文件的目的是让springboot知道你需要自动配置的类都有哪些。如果你这个starter里面有多个Autoconfigure,可以采用这种方式隔开:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.kobelee.FirstAutoConfiguration,\
cn.kobelee.SecondAutoConfiguration

mvn install 略

到这一步,我们的自定义的starter已经创建好了。接下来我们去另一个springboot项目里面,去使用刚刚生成的test-spring-boot-starter-1.0-SNAPSHOT.jar

在项目中使用自定义starter

先在新项目中引入刚刚的jar包pom依赖:

<dependency>
    <groupId>cn.kobelee</groupId>
    <artifactId>test-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

application.yml里面定义我们starter需要的属性

kobelee:
  util:
    enabled: true
    configStr: I'm configStr

在新项目中引用SimpleStringConcater并且@Autowired注入。

package cn.kobelee.demo.controller;

import cn.kobelee.util.component.SimpleStringConcater;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
@RequestMapping("/")
public class TestController {

    @Autowired
    private SimpleStringConcater simpleStringConcater;

    @RequestMapping("test")
    public String test(){
        String concatStr = simpleStringConcater.concat("I'm param Str. ");
        System.out.println(concatStr);
        return concatStr;
    }
}

此时,我们回顾一下starter里面的simpleStringConcater的实例化条件:

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "kobelee.util", value = "enabled", havingValue = "true")

容器中确实没有这个bean,并且我在application.yml里面也在kobelee.util.enabled里面配置了true.
启动项目,访问Controller对应路径。发现控制台和浏览器返回都有了打印输出:

I'm param Str. I'm configStr

Conclusion 总结

本文以一个简单的StringConcat为例子,介绍了自定义spring-boot-starter涉及到的几个关键步骤。包括:
* pom依赖
* 实现类(业务逻辑)
* 配置类Properties和AutoConfiguration
* spring.factories
* mvn install 然后使用

适用场景

当需要有某些通用功能(比如MQ的使用、数据库的使用、和短信系统的接入等),而并不是特别个性化的业务,我们可以考虑做成一个starter来方便代码的复用。尤其当在编写架构层面的代码的时候,spring-boot-starter更是将这一点体现得淋漓尽致。

不时之需

spring.io官方提供了很多AutoConfigure,可以加入浏览器收藏夹以备不时之需。
https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/html/appendix-auto-configuration-classes.html#auto-configuration-classes

当考虑新建starter的时候,更好的方式是去github上搜索一下,看是否已经有“好事之徒”提前铺好了路。

实现自定义spring-boot-starter

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

滚动到顶部