琴瑟和鸣:定制化 Spring Boot Starter,让你的接口调用如行云流水般自然

本文最后更新于:3 个月前

心态决定命运,用积极的心态去面对生活,生活也会以同样的方式回应你。

破冰

2024 年 12 月 25 日

今年年初新增的栏目,当时正在为年后的日常实习做准备,计划定制化一个属于自己的 Spring Boot 初始模板来提升开发效率。

后来在短暂的日常实习过程中,也逐渐接触到了小组内企业级项目开发初始模板,更加规范和高效。

现在我已经坐在南京九龙湖国际公园的泰豪软件办公室内,正式接纳了自己人生的第一份正式工作。

实习入职的第三天开始,在阳哥的指导下我开始逐渐负责一整块后台管理项目开发,这段时间确实也积累了不少开发经验。

工作总会越来越顺利的,最近生活过得挺不错。

跨年倒计时,七天。

活在当下,热烈期盼着未来的生活,秉持着一往无前的信念,昂首阔步继续向前迈步,

愿,仕途顺利,前路坦荡。

风生,水起。

  • 在 SpringBoot 中 着手开发一个 stater,简要介绍 SDK 的开发流程

    🍖 推荐阅读:

Elijasmine 的个人主页 - 文章 - 掘金 (juejin.cn)

SpringBoot 系列(一) SpringBoot 启动流程 - 掘金 (juejin.cn)

面试题:谈谈 Spring 用到了哪些设计模式? - 掘金 (juejin.cn)

SpringBoot 的 starter 到底是什么? - 掘金 (juejin.cn)

阿里一面:说一说 Java、Spring、Dubbo 三者 SPI 机制的原理和区别 - 掘金 (juejin.cn)

三分钟了解 springBoot 之 spring.factories 扩展机制 - 掘金 (juejin.cn)

springboot 自定义 starter 的过程以及遇到的问题 - 简书 (jianshu.com)

正文

自定义 Starter

  • 之前我们开发过 memory-client-sdk 接口调用 SDK,但是不够完善,今天在保持原有功能不变的情况下,重构该 SDK:(2024/01/08 晚)

新建 Spring Boot 项目

  • 新建 Spring Boot 项目 memory-client-spring-boot-starter:
1
2
3
<groupId>com.memory</groupId>
<artifactId>memory-client-spring-boot-starter</artifactId>
<version>0.0.1</version>

依赖配置

  • pom.xml 文件下,新增如下依赖:
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>s
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
  • spring-boot-configuration-processor 在自定义 Spring Boot Starter 时的作用主要是生成配置元数据、提供代码提示和自动补全,以及确保配置属性的正确解析:
    • 生成配置元数据:该依赖会根据在项目中定义的带有 @ConfigurationProperties 注解的类,在 META-INF 文件夹下生成 spring-configuration-metadata.json 文件。这个文件是一种元数据文件,其中包含了关于配置属性的信息,如属性名称、类型、默认值等。这些信息可以用于在 IDE 中编辑配置文件时提供代码提示和自动补全等功能。
    • 提供代码提示和自动补全:当你在 IDE 中编辑配置文件时,由于 spring-boot-configuration-processor 生成的元数据,IDE 会提供代码提示和自动补全功能。这使得在编写配置文件时更加方便,降低了因拼写错误或配置项不正确而导致的错误。
    • 确保配置属性的正确解析spring-boot-configuration-processor 在编译时会对带有 @ConfigurationProperties 注解的类进行处理,确保配置属性能够被正确地解析和绑定。这对于自定义的 Starter 来说非常重要,因为正确的解析和绑定配置属性是保证 Starter 功能正常的前提。
  • 注意,新建的 Spring Boot 项目的pom.xml文件下,都会有build标签,记得移除 👇:
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
<!-- 移除该内容 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>

新增配置文件类

  • 在 properties 目录下,新增配置文件类 MemoryClientProperties:
1
2
3
4
5
6
7
8
@ConfigurationProperties(prefix = "memory.client")
public class MemoryClientProperties {
private String accessKey;
private String secretKey;

// 省略 Getter()、Setter() 方法
..............................
}
  • @ConfigurationProperties注解能够自动获取 application.properties 配置文件中前缀为 spring.girlfriend 节点下 message属性的内容

新增功能接口

  • 在 service 目录下,新增功能接口 MemoryClientService,用来实现对各个接口发起调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public interface MemoryClientService {
/**
* 获取随机名言
*
* @param words 名言类型
* @return 随机名言
*/
String getRandomWord(Words words);

/**
* 获取随机壁纸
*
* @param picture 壁纸类型
* @return 壁纸名言
*/
String getPictureListByType(Picture picture);
}

新增功能接口实现类

  • 在 service/impl 目录下,新增功能接口实现类 MemoryClientServiceImpl,用来实现对各个接口发起调用:
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
/**
* 获取随机名言
*
* @param words 名言类型
* @return 随机名言
*/
@Override
public String getRandomWord(Words words) {
String json = JSONUtil.toJsonStr(words);
return HttpRequest.post(GATEWAY_HOST + "/api/words/one/random")
.addHeaders(getHeaderMap(json))
.body(json)
.execute()
.body();
}

/**
* 获取随机壁纸
*
* @param picture 壁纸类型
* @return 壁纸名言
*/
@Override
public String getPictureListByType(Picture picture) {
String json = JSONUtil.toJsonStr(picture);
return HttpRequest.post(GATEWAY_HOST + "/api/wallpaper/list/page/vo")
.addHeaders(getHeaderMap(json))
.body(json)
.execute()
.body();
}

// 省略其他接口调用方法
.................................

新增自动配置类

  • 新增自动配置类 MemoryClientAutoConfiguration,实现自动化配置功能:
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ConditionalOnClass(MemoryClientService.class)
@EnableConfigurationProperties(MemoryClientProperties.class)
class MemoryClientAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public MemoryClientService memoryClientService() {
return new MemoryClientServiceImpl();
}
}
  • 简单介绍下这几个注解的作用:
    • @Configuration: 标注类为一个配置类,让 spring 去扫描它;
    • @ConditionalOnClass:条件注解,只有在 classpath 路径下存在指定 class 文件时,才会实例化 Bean
    • @EnableConfigurationProperties:使指定配置类生效;
    • @Bean: 创建一个实例类注入到 Spring Ioc 容器中;
    • @ConditionalOnMissingBean`:条件注解,意思是,仅当 Ioc 容器不存在指定类型的 Bean 时,才会创建 Bean。

配置自动装配类路径

  • 配置自动装配的类的路径,这样 Spring Boot 会在启动时,自动会去查找指定文件 /META-INF/spring.factories,若有,就会根据配置的类的全路径去自动化配置:

  • 在 Spring Boot 2.x 中,在 resource/META-INF/spring.factories 文件下,添加如下配置来标记自动配置类:

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.starter3.NameAutoConfiguration
  • 而在 Spring Boot 3.x 中,在 resource/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件下,添加如下配置:
1
com.example.starter3.NameAutoConfiguration

打包

  • 是将 girl-friend-spring-boot-starter 打成 jar 包,放到本地的 maven 仓库中去在项目根路径下执行 maven 命令:
1
mvn clean install

引用自定义 Starter

  • 在需要引入 memory-client-spring-boot-starter 接口调用功能的 Spring Boot 项目中的 pom.xml文件中,导入依赖:
1
2
3
4
5
<dependency>
<groupId>com.memory.client</groupId>
<artifactId>memory-backend-server-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
  • resouce目录下的application.yaml配置文件下,添加如下配置:
1
2
3
4
memory-api:
client:
access-key: memory
secret-key: 12345678
  • 注入 MemoryClientService,可以对任一接口服务发起调用:
1
2
3
4
5
6
7
8
9
10
11
12
@Resource
private MemoryClientService memoryClientService;

// 随机名言
com.memory.client.model.Words words = gson.fromJson(userRequestParams, com.memory.client.model.Words.class);
result = memoryClientService.getRandomWord(words);

// 随机壁纸
com.memory.client.model.Picture picture = gson.fromJson(userRequestParams, com.memory.client.model.Picture.class);
result = memoryClientService.getPictureListByType(picture);

............................

基本流程

  • 开发一个 stater 只要做到:
    • 构建项目,初始化依赖
    • 编写接口方法

着手开发

  • 新建一个项目,项目依赖中勾选如下依赖,如图所示:
1
Spring Configuration Processer

image-20230805173935211

  • 移除 build,否则会报错(可选择性移除 test 依赖):

image-20230805174043426

image-20230805174030412

  • 启动类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@ConfigurationProperties("memory-api.client")
@Data
@ComponentScan
class MemoryClientConfig {
private String access_key;
private String secret_key;

@Bean
public MemoryClient memoryClient(){
// return new MemoryClient(access_key,secret_key);
return new MemoryClient();
}
}
  • resources/META-INF/spring.factories 下:
1
2
# spring boot starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.memory.clientsdk.MemoryClientConfig
  • 构建为 starter:

image-20230805175422289

  • 我们可以使用这个 starter 了,导入依赖:
1
2
3
4
5
<dependency>
<groupId>com.memory</groupId>
<artifactId>memory-client-sdk</artifactId>
<version>0.0.1</version>
</dependency>
  • yaml 配置:
1
2
3
4
memory-api:
client:
access-key: memory
secret-key: 12345678
  • 测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class MemoryClientApplicationTests {
@Resource
private MemoryClient memoryClient;

@Test
void contextLoads() {
System.out.println("成功了");
memoryClient.getNameByGet("邓哈哈");
memoryClient.getNameByPost("邓嘻嘻");

User user = new User("邓尼玛");
memoryClient.getUserByPost(user);
}
}

踩坑经历

测试类上方要添加 @SpringBootTest 注解

Spring Boot 版本问题

  • 之前做这个,没有考虑到 Spring Boot 的版本问题(2024/01/08 早)
  • 在 Spring Boot 2.x 中,在 resource/META-INF/spring.factories 文件下,添加如下配置来标记自动配置类:
1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.starter3.NameAutoConfiguration
  • 而在 Spring Boot 3.x 中,在 resource/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件下,添加如下配置:
1
com.example.starter3.NameAutoConfiguration

接口文档

2024 年 12 月 19 日

所有模块接口的文档都拿到了,网页均保存至本地集锦。

image-20241219110033332

image-20241219175257598

Springboot 系列(十六)你真的了解 Swagger 文档吗?-腾讯云开发者社区-腾讯云 (tencent.com.cn)

2024 年 12 月 24 日

一篇文章带你搞定 SpringBoot 整合 Swagger2_Java 开发学习最全合集-CSDN专栏

springboot整合swagger2_spring boot swagger2-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
<!--swagger相关-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger-version}</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("许可证管理接口文档")
.description("许可证管理接口文档")
.version("1.0")
.build();
}
}

image-20241224175735003

org.springframework.context.ApplicationContextException: Failed to start bean ‘documentationPluginsBootstrapper’; nested exception is java.lang.NullPointerException - 哩个啷个波 - 博客园 (cnblogs.com)

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
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.24.jar:5.3.24]
at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_211]
at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.24.jar:5.3.24]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.7.jar:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.7.jar:2.7.7]
at com.license.server.LicenseServerApplication.main(LicenseServerApplication.java:10) [classes/:na]
Caused by: java.lang.NullPointerException: null
at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:112) ~[springfox-spi-2.9.2.jar:null]
at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:109) ~[springfox-spi-2.9.2.jar:null]
at com.google.common.collect.ComparatorOrdering.compare(ComparatorOrdering.java:37) ~[guava-20.0.jar:na]
at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:1.8.0_211]
at java.util.TimSort.sort(TimSort.java:220) ~[na:1.8.0_211]
at java.util.Arrays.sort(Arrays.java:1438) ~[na:1.8.0_211]
at com.google.common.collect.Ordering.sortedCopy(Ordering.java:855) ~[guava-20.0.jar:na]
at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:57) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:138) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:135) ~[springfox-spring-web-2.9.2.jar:null]
at com.google.common.collect.Iterators$7.transform(Iterators.java:750) ~[guava-20.0.jar:na]
at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na]
at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na]
at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:52) ~[guava-20.0.jar:na]
at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:50) ~[guava-20.0.jar:na]
at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:249) ~[guava-20.0.jar:na]
at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:209) ~[guava-20.0.jar:na]
at com.google.common.collect.FluentIterable.toList(FluentIterable.java:614) ~[guava-20.0.jar:na]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.defaultContextBuilder(DocumentationPluginsBootstrapper.java:111) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.buildContext(DocumentationPluginsBootstrapper.java:96) ~[springfox-spring-web-2.9.2.jar:null]
at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:167) ~[springfox-spring-web-2.9.2.jar:null]
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.24.jar:5.3.24]
... 14 common frames omitted

org.springframework.context.ApplicationContextException: Failed to start bean ‘documentationPluginsB-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("许可证管理接口文档")
.description("许可证管理接口文档")
.version("1.0")
.build();
}
}

2024 年 12 月 25 日

怪了,昨晚能成功启动项目了,但接口文档怎么还是访问不了呢,其他接口访问正常,看来还是整合 Swagger 失败了。

image-20241225090517410

一篇文章带你搞定 SpringBoot 整合 Swagger2_Java 开发学习最全合集-CSDN专栏

解决springboot接入springfox-swagger2遇到的一些问题_java_脚本之家 (jb51.net)

image-20241225093647424

就这个文档对症下药了,果然是 Spring Boot 较高版本(2.6以上)整合 Swagger2 就会出现报错,三种解决方案:

1、ShortVideoSwagger2的配置类需要集成WebMvcConfigurationSupport

2、ShortVideoSwagger2的配置类增加@EnableWebMvc注解

3、springboot的配置增加增加一下配置

1
spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER

通过试验得知,这三种接口都可以解决,但是前两种是有副总用的

@EnableWebMvc建议慎用,最后在非springboot项目中使用

前两种解决方案会破坏springboot对springwebmvc的自动装配,导致自定义的一些convertor或者ObjectMapper失效。

目前我的项目中是自定义的ObjectMapper失效。

所以最后使用第三种方式,后期的springboot版本的matching-strategy默认的改为了PATH_PATTERN_PARSER,把它改为ANT_PATH_MATCHER就可以了。

这下子项目确实能够正常启动了,并且 Swagger 文档还特么生效了,完美解决问题,总结下 SpringBoot 集成 Swagger2 的步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<properties>
<swagger-version>2.9.2</swagger-version>
<swagger-ui.version>1.9.6</swagger-ui.version>
</properties>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger-version}</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("许可证管理接口文档")
.description("许可证管理接口文档")
.version("1.0")
.build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@Api(value = "License 授权校验", tags = "License 授权校验")
@RequestMapping("/license")
public class LicenseVerifyController {
@Resource
private LicenseVerifyService licenseVerifyService;

/**
* License 证书上传
*
* @return LicenseContent
*/
@ApiOperation(value = "License 证书上传", notes = "License 证书上传")
@PostMapping("/upload")
public CommonResult<Boolean> upload(MultipartFile file) {

........................................
}

......................................
}

如果出现报错就按照上面提到的方案,配置文件里增加配置即可一步到位解决,现在接口文档总算可正常查看了。

1
spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER

image-20241225094907426

总结


琴瑟和鸣:定制化 Spring Boot Starter,让你的接口调用如行云流水般自然
https://test.atomgit.net/blog/2023/08/05/琴瑟和鸣:定制化 Spring Boot Starter,让你的接口调用如行云流水般自然/
作者
Memory
发布于
2023年8月5日
更新于
2024年12月25日
许可协议