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

WU双

共 18100字,需浏览 37分钟

 · 2023-10-14

不好意思,是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配置的,如下所示:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"    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">
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
<!-- more bean definitions go here -->
</beans>
大家是不是好久没见过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文件依赖如下:
<?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">  <!-- This module was also published with a richer model, Gradle metadata,  -->  <!-- which should be used instead. Do not delete the following line which  -->  <!-- is to indicate to Gradle or any Gradle module metadata file consumer  -->  <!-- that they should prefer consuming it instead. -->  <!-- do_not_remove: published-with-gradle-metadata -->  <modelVersion>4.0.0</modelVersion>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-data-redis</artifactId>  <version>2.4.2</version>  <name>spring-boot-starter-data-redis</name>  <description>Starter for using Redis key-value data store with Spring Data Redis and the Lettuce client</description>  <url>https://spring.io/projects/spring-boot</url>  <organization>    <name>Pivotal Software, Inc.</name>    <url>https://spring.io</url>  </organization>  <licenses>    <license>      <name>Apache License, Version 2.0</name>      <url>https://www.apache.org/licenses/LICENSE-2.0</url>    </license>  </licenses>  <developers>    <developer>      <name>Pivotal</name>      <email>info@pivotal.io</email>      <organization>Pivotal Software, Inc.</organization>      <organizationUrl>https://www.spring.io</organizationUrl>    </developer>  </developers>  <scm>    <connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection>    <developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git</developerConnection>    <url>https://github.com/spring-projects/spring-boot</url>  </scm>  <issueManagement>    <system>GitHub</system>    <url>https://github.com/spring-projects/spring-boot/issues</url>  </issueManagement>  <dependencies>    <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter</artifactId>      <version>2.4.2</version>      <scope>compile</scope>    </dependency>    <dependency>      <groupId>org.springframework.data</groupId>      <artifactId>spring-data-redis</artifactId>      <version>2.4.3</version>      <scope>compile</scope>    </dependency>    <dependency>      <groupId>io.lettuce</groupId>      <artifactId>lettuce-core</artifactId>      <version>6.0.2.RELEASE</version>      <scope>compile</scope>    </dependency>  </dependencies></project>
可以看到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<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> 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> T get(String key, Class<T> 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模块编写完成并且打包上传到仓库后,其他项目只需引入相应的依赖,就能实现自动配置运行定时任务处理的功能。
依赖示例如下:
<dependency>   <groupId>com.example</groupId>   <artifactId>scheduler-spring-boot-starter</artifactId>   <version>${scheduler-spring-boot-starter-version}</version></dependency>
依赖后,项目无需任何处理,将自动运行定时任务:
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在不同环境中的自动配置情况,非常的简单高效。



写在最后

我们从Spring Boot的自动配置讲起,举例一步步讲解了spring-boot-autoconfigure如何与spring-boot-starter-xxx模块配合,进而完成自动配置的。
我们还引出了Spring的Beans概念,代码中的所有被Spring IoC容器所管理的实例,就叫做Beans。
xml配置、注解配置也好,自动配置也罢,实际都是配置的Spring的Beans。
我们还带领大家一步步地完成了自定义的scheduler-spring-boot-starter开发。
最后讲解了如何通过模拟不同的集成环境,来测试自己的starter。
希望今天的内容对大家有所帮助,完整项目代码请关注公众号:WU双,对话框回复【starter】即可获取。
推荐阅读:
《微服务不同环境到底该如何部署?最佳实践是什么?》



聊技术,不止于技术。

在这里我会分享技术文章、管理知识以及个人的思想感悟,欢迎点击关注。



浏览 58
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报