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中配置的配置类,所以这便是上述第一点问题。

configurations

  至于上述第三点,原理便是@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,如下:
    social-starter的pom依赖
    主工程的pom依赖

    但是对于以上解决方案,我发现我的没有任何改变(对不起,我上边只是多提供一些解决手段,不是故意啰嗦),而我这边出现此问题的原因好像就是上边所说的,貌似启动了两个容器,主容器之所以找不到这个class,是因为这个class被注入到了starter中的容器中。要解决此问题,还是要干掉spring-boot-maven-plugin插件和starter的启动类。

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

问题描述

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

问题结果

  我参考了mybatisplus的starter以及其他第三方的starter,发现他们都是直接只搞一个starter,autoconfigure直接就在这里边了。
mybatis-plus starter

我不死心,因为看到spring-boot-starter-redis等这些官方starter都是有对外暴露的starter,还区分实际执行的autoconfigure,总觉得这不符合规范,一直到在spring.io上看到了官方描述
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文件。

createSchema
getscripts

如果不是我画蛇添足的显示指定加载脚本路径,其实并不会出现无法执行starter中的脚本文件的问题。
由此,此问题的根本原因是classpath:和classpath
:的区别(前者为主项目的class路径,后者为包含第三方jar文件中进行查找)的问题。所以即使我显示配置schema为classpath*:schema.sql也是可以的。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343