springboot 自定义starter的过程以及遇到的问题

springboot 自定义starter的过程以及遇到的问题

前言

  现在微服务火的一塌糊涂,但凡出来面个试,好像你不会微服务就跟你什么都不会一样。但是像我们这种做外包的小公司,上微服务就不太现实,首先技术支撑不够,其次开发速度无法满足快速开发、快速交付的要求,各种服务治理、服务熔断和降级、链路追踪等等,简直能把甲方的规划进度拖死,而且根据我们的经验来说,找我们的甲方70%的都是没有自己的技术团队,这样交付后即使让甲方自己组建技术团队,对他们来说也是一笔更高的成本。
  所以在这种背景下,我们选择依托springboot的starter特性,把业务逻辑进行封装,使不同的甲方,重复的项目需求能够达到即插即拔,达到快速构建、快速开发,节省成本的目的。由此定义名字为spring-lego:像乐高积木一样,随意组合,快速成型。此项目持续集成中,以下便是在进行starter封装过程中遇到的问题以及注意的细节。


技术栈

名称 版本
springboot 2.2.0.RELEASE
mybatisPlus 3.2.0
redis 5.0.6
mysql 5.7
swagger 2.7.0

github:https://github.com/qismyq/spring-lego

starter结构

  • lego-frame-spring-boot-starter(基础starter,其它所有starter包括主项目直接依赖此starter即可,主工程无需在引入springboot-starter依赖。以下简称frame-starter)
    • lego-frame-spring-boot-starter
    • lego-frame-spring-boot-autoconfigure
  • lego-user-spring-boot-starter(用户业务starter,以下简称user-starter)
    • lego-user-spring-boot-starter
    • lego-user-spring-boot-autoconfigure
  • lego-shiro-spring-boot-starter(权限控制starter,需依赖user-starter,以下简称shiro-starter)
    • lego-shiro-spring-boot-starter
    • lego-shiro-spring-boot-autoconfigure

1. yml配置无法覆盖使用的问题

问题描述

  本身是希望如果其他starter或者主工程依赖frame-stater时,frame-starter中的yml配置文件可以直接生效或者覆盖生效,即如果主工程中不配置yml时,即使用frame-starter中的yml配置,如果主工程中配置了相同的配置,则进行覆盖操作,以主工程中的配置为生效配置。但是当我再主工程中只是配置了spring.server.port,而其他任何都没有配置时,发现项目启动失败,失败原因为无法创建datasource这个bean 。

问题结论

  此问题只能使用主工程中的yml文件,发现starter在install的时候好像相关的yml文件并没有被打包进去。即像mybatis或者jdbc一样,需要在主工程中进行配置。


2. starter中的autoConfig问题

问题描述

  网上很多资料对封装starter都是一个简单的示例,大多都是什么xxxServiceAutoConfiguration,然后加一堆@Configuration、@ConditionalOnxxx之类的注解,然后在spring.factories中配置该自动配置类。
  而我遇到的问题是在frame-starter中配置了不止一个autoConfig类,如:DuridConfig、MabatisPlusConfig、RedisConfig等。因为在单体应用中只是在这些配置类上增加@Configuration注解即可,启动时会自动生效,但是当我封装好starter后,偶然间发现这些配置类并没有再主工程的启动中生效。

解决方案

  经多次测试,关键问题有三点:

  1. 在frame-autoconfiguration的spring-factories文件中是否有明确指明自动配置类。
  2. 在frame-autoconfiguration的pom文件中是否有配置spring-boot-maven-plugin插件,此插件的作用是用于springboot依靠java -jar启动时可以找到主启动类,如果没有使用此插件,则打包后使用java -jar命令则会报错:“frame-spring-boot-autoconfigure-0.0.1-SNAPSHOT.jar中没有主清单属性”
  3. 引入方的主启动类是否可以扫描到frame-autoconfiguration中的自动配置类。

  其实三点中最关键的点是第三点,如果能保证主工程能够扫描到frame-starter中的配置类,则不需要关注第一和第二点。如果无法保证第三点,则建议使用第一点,保证在spring.factories文件中显式的指定要自动配置的类。至于第二点,如果在frame-autuconfiguration的pom中引入此插件,则在install时会找主启动类中的main方法,否则会提示"Unable to find main class",缺少主启动类
而当你第一点和第二点都存在的情况下,会报错“java.lang.IllegalStateException: Unable to read meta-data for class net.yunqihui.starter.config.DruidConfig”(此问题会在下边详细描述)。所以建议在封装starter时一定不要在pom中引用spring-boot-maven-plugin插件以及不要将主启动类打包进去

关于第一点:在spring.factories中显示指定这些配置类的目录,注意“,”后不要有空格之类的符合,不然会出现无法找到此bean的错误。

 # Auto Configure
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 net.yunqihui.starter.config.DruidConfig,\
 net.yunqihui.starter.config.web.SwaggerConfig,\
 net.yunqihui.starter.config.MessageConvertConfig,\
 net.yunqihui.starter.config.MybatisPlusConfig,\
 net.yunqihui.starter.config.RedisConfig,\
 net.yunqihui.starter.config.SchedulerConfig

关于第三点:在主工程的主启动类上增加compantScan注解,注解的值需要添加stater中的configBean的路径,以及主工程中的扫描包路径(注意,此路径不能丢

@SpringBootApplication
@MapperScan("net.yunqihui.**.mapper")
@EnableCaching
@ComponentScan({
     "net.yunqihui","net.yunqihui.config"})
public class SpringBoot08StarterTestApplication {
     
	public static void main(String[] args) {
     
		SpringApplication.run(SpringBoot08StarterTestApplication.class, args);
	}
}
问题结论

  一开始想的简单了,认为那些xxxServiceAutoConfiguration就相当于starter的入口而已,只要在spring.factories中配置了此配置类,即可达到starter中的其它的@configuration自动配置。其实并不是,根据springboot的自动配置原理,当开启了EnableAutoConfiguration(记住这个注解,这个是后续的关键)后,会扫描主启动类所在的包以及子包,但是通常的starter属于第三方包,包名并不一定和主工程的包名一致,自然无法像单体工程一样扫描到stater中的Bean。而boot是怎么扫描到上边举例的那些xxxServiceAutoConfiguration的呢?是通过spring.factories文件的内容进行查找class然后实现自动配置的。
还记得上边说的关键的EnabaleAutoConfiguration吗?他的注解上有个元注解@Import,此注解的AutoConfigurationImportSeclector.class中调用方法getCandidateConfigurations中会拿到spring.factories中配置的配置类,所以这便是上述第一点问题。
springboot 自定义starter的过程以及遇到的问题_第1张图片
  至于上述第三点,原理便是@ComponentScan,它会让主启动类扫描当前所在包以及子包,所以如果你的主启动类是在包net.yunqihui下,同样各种starter的Bean也是再net.yunqihui包或者子包下,则无需指定spring.factories也可以将配置类加载进容器中,甚至@Controller等各种component组件均可(关键点不在于starter是否是第三方包,而在于扫描路径)。

注意: 如果第二点和第三点同时存在,你会发现请求starter中的controller时会有404状态的错误,这说明即使包路径相同,好像主工程并没有将starter中的component注入进容器,在我看来,好像并不是没有被注入,而是在启动主工程的主启动类时,启动的是两个容器(主工程容器和starter工程容器),而starter中的component被注册到了后者中。所以,建议任何starter中都不要使用spring-boot-maven-plugin插件,并且在install时把主启动类排除在外


3. java.lang.IllegalStateException: Unable to read meta-data for class xxx.xxx.xxx问题

问题描述

封装starter时,如果你在spring.factories中显示指定了某些自动配置类,并且在pom文件中使用了spring-boot-maven-plugin插件,且install时将主启动类打包进去,则在主工程(也即引入方)启动时,会报如下错误:

java.lang.IllegalStateException: Unable to read meta-data for class net.yunqihui.starter.config.DruidConfig
	at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getAnnotationMetadata(AutoConfigurationSorter.java:233) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getOrder(AutoConfigurationSorter.java:204) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.access$000(AutoConfigurationSorter.java:150) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.autoconfigure.AutoConfigurationSorter.lambda$getInPriorityOrder$0(AutoConfigurationSorter.java:62) ~[spring-boot-autoconfigure-2.1.8.RELEASE.jar:2.1.8.RELEASE]
	at org.springframework.boot.autoconfigure.AutoConfigurationSorter$$Lambda$178/100708535.compare(Unknown Source) ~[na:na]
	at java.util.TimSort.countRunAndMakeAscending(Unknown Source) ~[na:1.8.0_25]
解决方案

关于此错误,有很多资料会有以下几点说法:

  1. spring.factories中有一些肉眼不可见的错误,比如“,”后有不小心多出一个空格的
  2. starter间的传递依赖,导致无法找到此class,比如主工程依赖frame-starter,social-starter也依赖frame-starter,但是同时主工程也依赖了social-starter,这个时候会出现重复依赖的问题,那么则需要将social-stater的依赖排除掉frame-starter,如下:
    springboot 自定义starter的过程以及遇到的问题_第2张图片springboot 自定义starter的过程以及遇到的问题_第3张图片
    但是对于以上解决方案,我发现我的没有任何改变(对不起,我上边只是多提供一些解决手段,不是故意啰嗦),而我这边出现此问题的原因好像就是上边所说的,貌似启动了两个容器,主容器之所以找不到这个class,是因为这个class被注入到了starter中的容器中。要解决此问题,还是要干掉spring-boot-maven-plugin插件和starter的启动类。

4. 到底是分两个模块还是一个模块

问题描述

  一开始我所了解的自定义一个starter,是需要一个auto configure模块和一个starter模块的,即前者负责具体的处理,而starter模块只是一个空壳,只是对外依赖的一个门户而已。但是后来当我先发布到中央仓库上时,发现需要将两个模块都进行打包发布,我就有点开始怀疑了,这样做的意义是什么,那么有人使用时直接依赖autoconfigure,而不使用starter模块不是浪费了吗?

问题结果

  我参考了mybatisplus的starter以及其他第三方的starter,发现他们都是直接只搞一个starter,autoconfigure直接就在这里边了。springboot 自定义starter的过程以及遇到的问题_第4张图片
我不死心,因为看到spring-boot-starter-redis等这些官方starter都是有对外暴露的starter,还区分实际执行的autoconfigure,总觉得这不符合规范,一直到在spring.io上看到了官方描述springboot 自定义starter的过程以及遇到的问题_第5张图片
既然官方都这样说了,那也没什么纠结的了,直接更改为一个module。

5. starter中的schema.sql无法执行

问题描述

  springboot中提供应用程序启动时执行sql脚本,通常为在yml中添加spring.datasource.initialization-mode: always(2.x版本),这样在应用程序启动时找classpath:schema.sql(我一开始的显式配置)文件。我的本意是在每个业务starter中配置相应的schema.sql,用来创建相应业务表结构。但是测试时发现,并没有执行自定义starter中的相关sql脚本。

解决方案

  其实这个问题完全跟我画蛇添足有关,springboot的DataSourceInitializer.class中的createSchema()虽然会传入spring.datasource.schema的配置,且默认fallback参数为schema。,但是getScripts()中会先判断resources是否为null,不为null,则执行显示配置的文件路径,当发现配置文件中没有相关配置时,才会查找classpath*:下的schema.sql和schema-all.sql文件。springboot 自定义starter的过程以及遇到的问题_第6张图片springboot 自定义starter的过程以及遇到的问题_第7张图片
如果不是我画蛇添足的显示指定加载脚本路径,其实并不会出现无法执行starter中的脚本文件的问题。
由此,此问题的根本原因是classpath:和classpath*:的区别(前者为主项目的class路径,后者为包含第三方jar文件中进行查找)的问题。所以即使我显示配置schema为classpath*:schema.sql也是可以的。


springboot 自定义starter的过程以及遇到的问题_第8张图片

你可能感兴趣的:(java,springboot,springboot,starter,自定义,问题)