前言
现在微服务火的一塌糊涂,但凡出来面个试,好像你不会微服务就跟你什么都不会一样。但是像我们这种做外包的小公司,上微服务就不太现实,首先技术支撑不够,其次开发速度无法满足快速开发、快速交付的要求,各种服务治理、服务熔断和降级、链路追踪等等,简直能把甲方的规划进度拖死,而且根据我们的经验来说,找我们的甲方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后,偶然间发现这些配置类并没有再主工程的启动中生效。
解决方案
经多次测试,关键问题有三点:
其实三点中最关键的点是第三点,如果能保证主工程能够扫描到frame-starter中的配置类,则不需要关注第一和第二点。如果无法保证第三点,则建议使用第一点,保证在spring.factories文件中显式的指定要自动配置的类。至于第二点,如果在frame-autuconfiguration的pom中引入此插件,则在install时会找主启动类中的main方法,否则会提示"Unable to find main class",
- 在frame-autoconfiguration的spring-factories文件中是否有明确指明自动配置类。
- 在frame-autoconfiguration的pom文件中是否有配置spring-boot-maven-plugin插件,此插件的作用是用于springboot依靠java -jar启动时可以找到主启动类,如果没有使用此插件,则打包后使用java -jar命令则会报错:“frame-spring-boot-autoconfigure-0.0.1-SNAPSHOT.jar中没有主清单属性”。
- 引入方的主启动类是否可以扫描到frame-autoconfiguration中的自动配置类。
而当你第一点和第二点都存在的情况下,会报错“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中配置的配置类,所以这便是上述第一点问题。
至于上述第三点,原理便是@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]
解决方案
关于此错误,有很多资料会有以下几点说法:
- spring.factories中有一些肉眼不可见的错误,比如“,”后有不小心多出一个空格的
- starter间的传递依赖,导致无法找到此class,比如主工程依赖frame-starter,social-starter也依赖frame-starter,但是同时主工程也依赖了social-starter,这个时候会出现重复依赖的问题,那么则需要将social-stater的依赖排除掉frame-starter,如下:
但是对于以上解决方案,我发现我的没有任何改变(对不起,我上边只是多提供一些解决手段,不是故意啰嗦),而我这边出现此问题的原因好像就是上边所说的,貌似启动了两个容器,主容器之所以找不到这个class,是因为这个class被注入到了starter中的容器中。要解决此问题,还是要干掉spring-boot-maven-plugin插件和starter的启动类。
4. 到底是分两个模块还是一个模块
问题描述
一开始我所了解的自定义一个starter,是需要一个auto configure模块和一个starter模块的,即前者负责具体的处理,而starter模块只是一个空壳,只是对外依赖的一个门户而已。但是后来当我先发布到中央仓库上时,发现需要将两个模块都进行打包发布,我就有点开始怀疑了,这样做的意义是什么,那么有人使用时直接依赖autoconfigure,而不使用starter模块不是浪费了吗?
问题结果
我参考了mybatisplus的starter以及其他第三方的starter,发现他们都是直接只搞一个starter,autoconfigure直接就在这里边了。我不死心,因为看到spring-boot-starter-redis等这些官方starter都是有对外暴露的starter,还区分实际执行的autoconfigure,总觉得这不符合规范,一直到在spring.io上看到了官方描述
既然官方都这样说了,那也没什么纠结的了,直接更改为一个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文件。
如果不是我画蛇添足的显示指定加载脚本路径,其实并不会出现无法执行starter中的脚本文件的问题。
由此,此问题的根本原因是classpath:和classpath:的区别(前者为主项目的class路径,后者为包含第三方jar文件中进行查找)的问题。所以即使我显示配置schema为classpath*:schema.sql也是可以的。