如何从零开始写一个xxx-spring-boot-stop

来源:WU双


不好意思,是xxx-spring-boot-starter。

Spring Boot与Spring最大的区别就是自动配置了,那你知道Spring Boot是如何完成自动配置的吗?
我们又如何编写自己的xxx-spring-boot-starter,进而完成自定义的自动配置。
又要如何测试自定义的starter在不同集成环境下的表现呢?
今天的文章给大家一一道来。

1

从auto configuration说起

Spring Boot和Spring最大的区别在哪,答案是自动配置。
Spring的概念很大,这里的Spring只是指 Spring Framework。
实际现在的开发应该没有直接用Spring的了,大都是通过Spring Boot来间接使用Spring。
包括官方也是这么建议的。

If you are just getting started with Spring, you may want to begin using the Spring Framework by creating a Spring Boot-based application. Spring Boot provides a quick (and opinionated) way to create a production-ready Spring-based application. 

上面这段话的意思是建议你直接通过Spring Boot来开始。
我们下面将一步步地来讲解Spring Boot是如何做到自动配置的。
熟悉Spring的小伙伴应该都熟悉一个概念,叫做Beans。Beans是什么呢,在Spring中,你代码中的所有被Spring IoC容器所管理的实例,就叫做Beans。
Spring IoC容器,是Spring框架最核心的概念。
Spring IoC Container
Spring最初Bean是通过xml配置的,如下所示:











    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://www.springframework.org/schema/beans        https://www.springframework.org/schema/beans/spring-beans.xsd">
   
   
大家是不是好久没见过xml配置的方式了,说实话我也好久没见到了,哈哈。
当然Spring也支持通过代码注解的方式来配置Bean。








@Configurationpublic class AppConfig {
   @Bean    public TransferServiceImpl transferService() {        return new TransferServiceImpl();    }}

上面通过注解和xml配置的方式是等效的。
实际无论是何种配置方式,通过xml也好,通过@Bean代码配置也罢,还是通过自动配置(实际也是通过@Bean),最终都是将类的实例注入到Spring的容器中去,及IOC Container。
Spring Boot的自动配置,实际是通过spring-boot-autoconfigure模块配合其他spring-boot-starter-xxx模块来完成的。
spring-boot-autoconfigure
spring-boot-autoconfigure模块的spring.factories类中定义了众多的XXXAutoConfiguration类,在符合一定的条件的时候(比如判断Classpath中是否有相应的类,也就是项目中是否引入了相关的依赖,实际就是是否引入了相应的spring-boot-starter-xxx模块),则会自动进行相应的配置,这里的自动配置实际就是配置Spring容器中的Bean。
下面具体举例说明。

2

常见的spring-boot-starter-xxx

我们以spring-boot-starter-data-redis为例,看下该starter是如何与spring-boot-autoconfigure模块来配合,进而自动注入Redis相关Bean的。
我们打开spring-boot-starter-data-redis jar包,会发现jar包里什么代码都没有?
spring-boot-starter-data-redis jar
没错,是这样的,spring-boot-starter-data-redis模块无需有任何代码,只需要有依赖就可以了。这与官方的文档也是一致的。

1. The autoconfigure module that contains the auto-configuration code for "acme". 2. The starter module that provides a dependency to the autoconfigure module as well as "acme" and any additional dependencies that are typically useful.

上面文档说明,完整的starter一般是由两部分组成,一个是autoconfigure模块,另一个是starter模块,autoconfigure模块包含自动配置的代码,starter模块只包括依赖引用。
对应这里的示例,autoconfigure模块对应的是spring-boot-autoconfigure,starter模块对应的是spring-boot-starter-data-redis。
那就让我们看看spring-boot-starter-data-redis包括了哪些引用,spring-boot-starter-data-redis的pom文件依赖如下:































































            4.0.0  org.springframework.boot  spring-boot-starter-data-redis  2.4.2  spring-boot-starter-data-redis  Starter for using Redis key-value data store with Spring Data Redis and the Lettuce client  https://spring.io/projects/spring-boot      Pivotal Software, Inc.    https://spring.io              Apache License, Version 2.0      https://www.apache.org/licenses/LICENSE-2.0                  Pivotal      info@pivotal.io      Pivotal Software, Inc.      https://www.spring.io            scm:git:git://github.com/spring-projects/spring-boot.git    scm:git:ssh://git@github.com/spring-projects/spring-boot.git    https://github.com/spring-projects/spring-boot        GitHub    https://github.com/spring-projects/spring-boot/issues              org.springframework.boot      spring-boot-starter      2.4.2      compile              org.springframework.data      spring-data-redis      2.4.3      compile              io.lettuce      lettuce-core      6.0.2.RELEASE      compile      
可以看到spring-boot-starter-data-redis模块主要是引入了Spring Redis及Lettuce相关的依赖,那么这些依赖到底是如何与spring-boot-autoconfigure配合从而完成自动注入的?
spring-boot-autoconfigure redis autoconfigure
spring-boot-autoconfigure模块中的spring.factories文件中,有关Redis的自动配置类如上,我们这里主要看RedisAutoConfiguration这个类。




























@Configuration(proxyBeanMethods = false)// 这里表示只有项目依赖中有RedisOperations这个类,下面的配置才会生效,正是因为我们引入了spring-boot-starter-data-redis依赖,然后项目中才会有RedisOperations类,所以该自动配置才会生效@ConditionalOnClass(RedisOperations.class)// RedisProperties为redis相关的配置,包括集群地址、连接池配置等信息都可以通过这个类来进行配置@EnableConfigurationProperties(RedisProperties.class)// 这里是导入了其他的配置类,为Redis连接池相关的配置@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })public class RedisAutoConfiguration {
   @Bean    @ConditionalOnMissingBean(name = "redisTemplate")    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {        RedisTemplate template = new RedisTemplate<>();        template.setConnectionFactory(redisConnectionFactory);        return template;    }
   @Bean    @ConditionalOnMissingBean    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {        StringRedisTemplate template = new StringRedisTemplate();        template.setConnectionFactory(redisConnectionFactory);        return template;    }
}
就是这个简单的配置类,将会自动帮我们完成Redis的Lettuce连接池Bean、Spring的RedisTemplate Bean和StringRedisTemplate Bean的注入,无需我们写一行代码。
反应在实际的项目编码中,我们在引入了spring-boot-starter-data-redis依赖后,无需编写一行代码,就可以直接使用RedisTemplate来操作Redis了,示例如下:























@Servicepublic class CacheService {        // 因为Spring Boot已经自动注入了StringRedisTemplate,所以这里我们代码里直接使用即可    @Autowired    private StringRedisTemplate redisTemplate;
   public void set(String key, Object value) {        redisTemplate.opsForValue().set(key, JSON.toJSONString(value));    }
   public void set(String key, Object value, long expireTimeout, TimeUnit timeUnit) {        redisTemplate.opsForValue().set(key, JSON.toJSONString(value), expireTimeout, timeUnit);    }
   public T get(String key, Class clazz) {        return JSON.parseObject(redisTemplate.opsForValue().get(key), clazz);    }
   public void delete(String key) {        redisTemplate.delete(key);    }}
可以看到,Spring通过spring-boot-autoconfigure模块配合其他spring-boot-starter-xxx模块从而达成的自动配置,极大的方便了项目的开发。
当然这些技术细节隐藏在背后,看似方便实际对开发的要求反而更高,毕竟在我看来,优秀的程序员不应该只知道闷头用,还要知道背后的原理。

3

写一个自己的

xxx-spring-boot-starter

既然Spring Boot的starter这么好用,那么我们如何写个自己的starter呢?
当然我们在写自己的starter之前,应该了解starter的目标及使用场景,在我看来starter的主要使用场景就是方便项目开发及集成,减少冗余代码。
官方建议starter需要包含configure模块及starter模块,一个模块负责自动配置的代码,另一个模块只负责依赖的引入,但如果我们的starter需求简单的话,也无需分成两个模块,写在一个模块也就可以了。
3.1 关于starter的命名
Spring官方的starter命名规范为spring-boot-starter-xxx,非官方的starter命名规范为xxx-spring-boot-starter。
像jasypt-spring-boot-starter、knife4j-spring-boot-starter等都是常见的第三方starter。
所以我们如果要自己写starter一定要遵循相应的规范。
在这节的示例中,我将会写一个定时任务的starter,命名为scheduler-spring-boot-starter,该starter在其他模块引入后将会自动开启运行特定的定时任务,同时支持任务名称和运行间隔时间的自定义配置。
3.2 scheduler-spring-boot-starter关键代码
直接看代码,关键代码如下,完整项目代码获取方式在文末。
SchedulerProperties配置类:





























@ConfigurationProperties(prefix = "scheduler")public class SchedulerProperties {
   /**     * 定时任务调度时间,单位ms,默认值1000ms     */    private long period = 1000L;
   /**     * 定时任务名称     */    private String taskName;
   public long getPeriod() {        return period;    }
   public void setPeriod(long period) {        this.period = period;    }
   public String getTaskName() {        return taskName;    }
   public void setTaskName(String taskName) {        this.taskName = taskName;    }}
SchedulerAutoConfiguration自动配置类:










@Configuration@EnableConfigurationProperties(SchedulerProperties.class)@Import(SchedulerExecutorConfiguration.class)public class SchedulerAutoConfiguration {
   @Bean    public Scheduler schedulerTask(ScheduledThreadPoolExecutor scheduledThreadPoolExecutor, SchedulerProperties schedulerProperties) {        return new Scheduler(scheduledThreadPoolExecutor, schedulerProperties);    }}
SchedulerExecutorConfiguration配置类:








@Configurationpublic class SchedulerExecutorConfiguration {
   @Bean    public ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() {        return new ScheduledThreadPoolExecutor(1);    }}
Scheduler类:



















public class Scheduler {
   public static final Logger LOGGER = LoggerFactory.getLogger(Scheduler.class);
   private final ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;    private final SchedulerProperties schedulerProperties;
   public Scheduler(ScheduledThreadPoolExecutor scheduledThreadPoolExecutor, SchedulerProperties schedulerProperties) {        this.scheduledThreadPoolExecutor = scheduledThreadPoolExecutor;        this.schedulerProperties = schedulerProperties;        this.init();    }
   public void init() {        scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {            LOGGER.info("scheduler task [{}], period [{}ms], currentTime [{}]", schedulerProperties.getTaskName(), schedulerProperties.getPeriod(), LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));        }, 0, schedulerProperties.getPeriod(), TimeUnit.MILLISECONDS);    }}
spring.factories配置文件:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.example.scheduler.SchedulerAutoConfiguration
scheduler-spring-boot-starter模块编写完成并且打包上传到仓库后,其他项目只需引入相应的依赖,就能实现自动配置运行定时任务处理的功能。
依赖示例如下:





   com.example   scheduler-spring-boot-starter   ${scheduler-spring-boot-starter-version}
依赖后,项目无需任何处理,将自动运行定时任务:




2023-10-11 14:22:20.683  INFO 25680 --- [pool-1-thread-1] com.example.scheduler.Scheduler          : scheduler task [test-example], period [5000ms], currentTime [2023-10-11 14:22:20]2023-10-11 14:22:25.689  INFO 25680 --- [pool-1-thread-1] com.example.scheduler.Scheduler          : scheduler task [test-example], period [5000ms], currentTime [2023-10-11 14:22:25]2023-10-11 14:22:30.685  INFO 25680 --- [pool-1-thread-1] com.example.scheduler.Scheduler          : scheduler task [test-example], period [5000ms], currentTime [2023-10-11 14:22:30]2023-10-11 14:22:35.681  INFO 25680 --- [pool-1-thread-1] com.example.scheduler.Scheduler          : scheduler task [test-example], period [5000ms], currentTime [2023-10-11 14:22:35]
并且还可以通过配置文件来灵活配置任务名和定时任务调度时间:
配置
可以看到配置项与IDEA集成的非常好,能够自动进行提示,这里是因为我们在starter模块中引入了spring-boot-configuration-processor,能够帮我们生成spring-configuration-metadata.json文件:
























{  "groups": [    {      "name": "scheduler",      "type": "com.example.scheduler.SchedulerProperties",      "sourceType": "com.example.scheduler.SchedulerProperties"    }  ],  "properties": [    {      "name": "scheduler.period",      "type": "java.lang.Long",      "description": "定时任务调度时间,单位ms",      "sourceType": "com.example.scheduler.SchedulerProperties"    },    {      "name": "scheduler.task-name",      "type": "java.lang.String",      "description": "定时任务名称",      "sourceType": "com.example.scheduler.SchedulerProperties"    }  ],  "hints": []}
到这里,我们就完成了自定义的xxx-spring-boot-starter开发,示例虽然比较简单,但麻雀虽小,五脏俱全,这里给大家以参考。

4

测试你的spring-boot-starter


除了真正的将starter引入到项目中进行集成测试外,starter模块可以自测吗?
毕竟外部项目非常的复杂,starter在不同的项目集成环境中可能表现的不一样,那么我们怎么进行starter自测,来看starter在不同环境下的表现呢?
我们可以通过模拟不同的运行环境,来测试starter相应的表现,实际就是测试相应的自动配置有没有生效。
还是直接看代码:




























class SchedulerAutoConfigurationTest {    // 通过contextRunner来模拟运行环境,这里是模拟配置了SchedulerAutoConfiguration类的应用环境,实际也就是引用了scheduler-spring-boot-starter后生效的配置    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()            .withConfiguration(AutoConfigurations.of(SchedulerAutoConfiguration.class));
   @Test    void testAutoConfiguration() {        this.contextRunner.run((context) -> {            // 测试自动配置有没有注入ScheduledThreadPoolExecutor Bean            assertThat(context).hasSingleBean(ScheduledThreadPoolExecutor.class);            // 测试自动配置有没有注入SchedulerProperties Bean            assertThat(context).hasSingleBean(SchedulerProperties.class);            // 测试自动配置有没有注入Scheduler Bean            assertThat(context).hasSingleBean(Scheduler.class);        });    }
   @Test    void testProperties() {        // 模拟环境配置了相应的参数        this.contextRunner.withPropertyValues("scheduler.period=5000", "scheduler.task-name=test-example")                .run((context) -> {                    // 测试对应参数设置是否生效                    assertThat(context.getBean(SchedulerProperties.class).getPeriod()).isEqualTo(5000);                    assertThat(context.getBean(SchedulerProperties.class).getTaskName()).isEqualTo("test-example");                });    }}
可以看到,通过模拟不同的集成环境,我们可以测试自定义starter在不同环境中的自动配置情况,非常的简单高效。

请使用浏览器的分享功能分享到微信等