如何开发一个starter

参考

starter的概念

  • starter是一种对依赖的synthesize(合成)
  • 进行一些默认的初始化,并且这些初始化操作,都可以在spring.yaml配置文件中覆盖
  • 进行一些实例的初始化,比如之前如果要实例化某类
    • 配置spring 扫描某包
    • 手动通过config创建bean
    • 而starter可通过spring.factories文件实例化对象

传统的做法

在没有starter之前,假如我想要在Spring中使用jpa,那我可能需要做以下操作:

  • 在Maven中引入使用的数据库的依赖(即JDBC的jar)
  • 引入jpa的依赖
  • 在xxx.xml中配置一些属性信息
  • 反复的调试直到可以正常运行

需要注意的是,这里操作在我们每次新建一个需要用到jpa的项目的时候都需要重复的做一次。也许你在第一次自己建立项目的时候是在Google上自己搜索了一番,花了半天时间解决掉了各种奇怪的问题之后,jpa终于能正常运行了。有些有经验的人会在OneNote上面把这次建立项目的过程给记录下来,包括操作的步骤以及需要用到的配置文件的内容,在下一次再创建jpa项目的时候,就不需要再次去Google了,只需要照着笔记来,之后再把所有的配置文件copy&paste就可以了

像上面这样的操作也不算不行,事实上我们在没有starter之前都是这么干的,但是这样做有几个问题:

  • 如果过程比较繁琐,这样一步步操作会增加出错的可能性
  • 不停地copy&paste不符合Don’t repeat yourself精神
  • 在第一次配置的时候(尤其如果开发者比较小白),需要花费掉大量的时间

使用Spring Boot Starter提升效率

starter的主要目的就是为了解决上面的这些问题。

starter的理念

starter会把所有用到的依赖都给包含进来,避免了开发者自己去引入依赖所带来的麻烦。需要注意的是不同的starter是为了解决不同的依赖,所以它们内部的实现可能会有很大的差异,例如jpa的starter和Redis的starter可能实现就不一样,这是因为starter的本质在于synthesize,这是一层在逻辑层面的抽象,也许这种理念有点类似于Docker,因为它们都是在做一个“包装”的操作,如果你知道Docker是为了解决什么问题的,也许你可以用Docker和starter做一个类比。

starter的实现

虽然不同的starter实现起来各有差异,但是他们基本上都会使用到两个相同的内容:ConfigurationProperties和AutoConfiguration。因为Spring Boot坚信“约定大于配置”这一理念,所以我们使用ConfigurationProperties来保存我们的配置,并且这些配置都可以有一个默认值,即在我们没有主动覆写原始配置的情况下,默认值就会生效,这在很多情况下是非常有用的。除此之外,starter的ConfigurationProperties还使得所有的配置属性被聚集到一个文件中(一般在resources目录下的application.properties),这样我们就告别了Spring项目中XML地狱。

starter的整体逻辑


上面的starter依赖的jar和我们自己手动配置的时候依赖的jar并没有什么不同,所以我们可以认为starter其实是把这一些繁琐的配置操作交给了自己,而把简单交给了用户。除了帮助用户去除了繁琐的构建操作,在“约定大于配置”的理念下,ConfigurationProperties还帮助用户减少了无谓的配置操作。并且因为 application.properties 文件的存在,即使需要自定义配置,所有的配置也只需要在一个文件中进行,使用起来非常方便

创建自己的Spring Boot Starter

创建starter的步骤

  • 创建一个starter项目,关于项目的命名你可以参考这里
  • 创建一个ConfigurationProperties用于保存你的配置信息(如果你的项目不使用配置信息则可以跳过这一步,不过这种情况非常少见)
  • 创建一个AutoConfiguration,引用定义好的配置信息;在AutoConfiguration中实现所有starter应该完成的操作,并且把这个类加入spring.factories配置文件中进行声明
  • 打包项目,之后在一个SpringBoot项目中引入该项目依赖,然后就可以使用该starter了

实施

新建maven项目example-spring-boot-starter

pom.xml中添加以下内容

<!-- 自定义starter都应该继承自该依赖 -->
  <!-- 如果自定义starter本身需要继承其它的依赖,可以参考 https://stackoverflow.com/a/21318359 解决 -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starters</artifactId>
    <version>1.5.2.RELEASE</version>
  </parent>

  <dependencies>
    <!-- 自定义starter依赖此jar包 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- lombok用于自动生成get、set方法 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.10</version>
    </dependency>
  </dependencies>

创建proterties类来保存配置信息

@ConfigurationProperties(prefix = "http") // 自动获取配置文件中前缀为http的属性,把值传入对象参数
@Setter
@Getter
public class HttpProperties {

    // 如果配置文件中配置了http.url属性,则该默认属性会被覆盖
    private String url = "http://www.baidu.com/";

}

上面这个类就是定义了一个属性,其默认值是 http://www.baidu.com/,我们可以通过在 application.properties中添加配置 http.url=https://www.zhihu.com 来覆盖参数的值。

创建业务类

@Setter
@Getter
public class HttpClient {

    private String url;

    // 根据url获取网页数据
    public String getHtml() {
        try {
            URL url = new URL(this.url);
            URLConnection urlConnection = url.openConnection();
            BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "utf-8"));
            String line = null;
            StringBuilder sb = new StringBuilder();
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            return sb.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "error";
    }
}

这个业务类的操作非常简单,只包含了一个 url 属性和一个 getHtml 方法,用于获取一个网页的HTML数据,读者看看就懂了。

创建AutoConfiguration

@Configuration
@EnableConfigurationProperties(HttpProperties.class)
public class HttpAutoConfiguration {

    @Resource
    private HttpProperties properties; // 使用配置

    // 在Spring上下文中创建一个对象
    @Bean
    @ConditionalOnMissingBean
    public HttpClient init() {
        HttpClient client = new HttpClient();

        String url = properties.getUrl();
        client.setUrl(url);
        return client;
    }

}

在上面的AutoConfiguration中我们实现了自己要求:在Spring的上下文中创建了一个HttpClient类的bean,并且我们把properties中的一个参数赋给了该bean。
关于@ConditionalOnMissingBean 这个注解,它的意思是在该bean不存在的情况下此方法才会执行,这个相当于开关的角色,更多关于开关系列的注解可以参考这里

配置spring.factories

最后,我们在 resources 文件夹下新建目录 META-INF,在目录中新建 spring.factories 文件,并且在 spring.factories 中配置AutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.kevin.HttpAutoConfiguration

到此,我们的starter已经创建完毕了,使用Maven打包该项目

mvn install

使用自定义starter

创建一个SpringBoot项目,在项目中添加我们之前打包的starter作为依赖,然后使用SringBoot来运行我们的starter

pom.xml中引入依赖

<dependency>
  <groupId>com.kevin</groupId>
  <artifactId>example-spring-boot-starter</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

新建Component类

@Component
public class RunIt {

    @Resource
    private HttpClient httpClient;

    public void hello() {
        System.out.println(httpClient.getHtml());
    }

}

新建接口,并执行RunIt代码

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

    @Autowired
    private RunIt runIt;

    @GetMapping
    public void test() {
        runIt.hello();
    }
}

测试成功

在starter或其它的依赖组件中,并不能直接通过component自动扫描,所以在starter中,通过spring.factories的方式实例化对象