ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot工程如何正确打包成SDK jar:关闭repackage与自动配置实践

SpringBoot工程如何正确打包成SDK jar:关闭repackage与自动配置实践 1. 先搞清楚一件事可执行jar和SDK jar根本不是同一个产物很多人第一次做SDK时会直接把自己跑得好好的SpringBoot项目mvn package一下把target目录里的jar丢给对方团队。对方一引入要么启动直接失败要么一大堆ClassNotFound要么明明引了却在调的时候报Bean不存在。问题出在哪你打的这个jar根本不是别人要的那种jar。SpringBoot的mvn package默认会触发spring-boot-maven-plugin的repackage把项目打成一个“可执行jar”。这种jar的内部结构是三层嵌套的你的业务类在BOOT-INF/classes里你的依赖jar在BOOT-INF/lib里而整个包的最外层是一个带Main-Class属性的启动器。它存在的目的是让你能通过java -jar一键启动并不是为了让别的项目把里面某个类依赖进来用。对方用Maven引入这个可执行jar后类加载器根本找不到BOOT-INF/classes下的类因为嵌套路径不满足常规jar的类查找约定。而SDK jar本质上就是一个普通jar包。它内部直接暴露包路径比如com/yourcompany/sdk/xxx.class依赖关系通过Maven的pom声明传递。别人引入后编译期能直接import运行期能直接被类加载器扫描到同时它的依赖能被Maven自动解析传递到调用方项目里。所以你要做的核心事情其实就是三个字拆掉repackage。把SpringBoot默认那种“打成一个独立可运行的胖jar”的模式改回“打一个普通的瘦jar同时保留Maven坐标和依赖声明”。这整篇博文要讲的就是这套流程里的关键动作以及我实际踩过的坑。1.1 为什么不能直接把SpringBoot可执行jar当SDK用我把之前做过的一个对接项目拿出来说。当时隔壁团队需要一个金额计算模块我这边SpringBoot写得挺顺Controller、Service、各种配置都有自测都通了。我就想着把这套东西打成一个包给他们调用省得他们重复造轮子。结果对方一引入就报错第一个异常是java.lang.NoClassDefFoundError指向我项目里用到的一个内部工具类。我当时第一反应是“依赖没传递”但查了半天发现不是依赖问题是他们根本就没能加载到任何业务类因为类都在BOOT-INF/classes下根本不是标准jar的类路径。后来我去看了maven依赖树里面确实有我的jar包坐标但依赖树里除了它自己没有任何一层子依赖被打进来。为什么因为spring-boot-maven-plugin在repackage时把项目原本的依赖信息都折叠进BOOT-INF/lib了生成的pom关联关系非常弱调用方Maven解析不到正确的依赖树于是一堆依赖缺失。这就能理解为什么说“SpringBoot默认打的jar是能跑的应用而不是能用的SDK”。要变成SDK就必须回到普通jar的打法让Maven坐标、依赖声明、类结构都符合JarSpec的基本约定而不是SpringBoot自定义的那套启动约定。1.2 SDK jar应该长什么样一个合格的SDK jar从结构上应该满足三个条件类文件直接以包结构存放能够被外部项目的类加载器直接扫描。pom文件里完整声明了它依赖了哪些第三方库以及这些依赖的作用域。不应该包含自己的配置文件、静态资源、页面模板这些应该交给调用方去定制。对应到SpringBoot工程里就是要把Controller这类Web层剥离掉把业务核心逻辑、工具类、封装好的客户端、自动配置类留下来。同时Maven构建时不要执行SpringBoot的repackage直接使用默认的jar插件就行。另外还有一个非常容易被忽略的点你的SDK引用了哪些SpringBoot版本会直接决定对方项目能不能顺利兼容。如果SDK内部用的是SpringBoot 2.7而调用方是SpringBoot 3.2在自动配置那块会出很大麻烦因为SpringBoot 3.x把javax迁移到了jakarta命名空间。所以做SDK时版本底盘要选兼容性好的或者尽可能做成不依赖具体版本的工具型SDK把依赖尽量控制在基础库级别。2. 打包前的工程改造从“能跑”到“能被依赖”如果只是改个pom把它打成普通jar其实意义不大。因为SpringBoot工程里那些Controller、启动类、配置类仍然会拖累调用方。调用方引入你的SDK后如果里面带了SpringBootApplication注解的启动类或者带了一堆RequestMapping的Controller他启动项目时会把你这些类全部扫进去轻则端口路由冲突重则因为扫描路径不同而多出一堆无意义Bean。我当时第一次做SDK就是直接把整个工程剥壳结果对方项目一启动日志里多出一堆乱七八糟的RequestMapping映射名有个接口还因为路径相同直接冲突启动失败。那次之后我开始总结做SDK前工程改造这件事比打包配置更重要。2.1 把核心逻辑和Web入口彻底分开最彻底的做法是改造成多模块工程结构长这样parent-project ├── core-sdk纯业务逻辑、工具类、客户端封装无Web依赖 ├── api-webSpringBoot启动工程依赖core-sdk提供HTTP接口给前端用 └── sdk-demo可选给调用方看的示例工程core-sdk就是你们要发布的SDK模块它不依赖spring-boot-starter-web也不需要main方法甚至不需要启动类。它只需要普通的Maven坐标、业务类、配置属性类就够了。api-web就是你们原来的SpringBoot应用依赖了core-sdk维持原有的接口能力。这样两个团队各取所需网页端还是通过HTTP接口调用其他服务如果需要更底层的Java能力接入就直接引core-sdk。如果不想拆多模块那也要在包里做好规划。把SDK要暴露的类统一放到com.xxx.sdk这个包路径里把Web层的类放到com.xxx.web下发布前在Maven配置里用excludes排除掉web相关类。但这种做法比较糙容易漏我建议有预算的话还是拆模块一劳永逸。2.2 main类该保留还是该删SDK jar里绝对不要出现SpringBootApplication注解的main类。原因有两个第一主类会被调用方的组件扫描扫到。虽然调用方扫描的是自己工程根包但如果他配置了ComponentScan(basePackages com.xxx)你的主类也会被扫描到这会让SpringBoot尝试加载一堆不必要的自动配置。第二主类一旦存在很容易让人误以为这个SDK需要单独启动这个打包姿势就跑偏了。正确的做法是SDK模块里不写main方法也不放SpringBootApplication类。配置入口使用SpringBoot自动配置机制来处理怎么写我放在第4小节里讲。2.3 配置文件交由调用方覆盖不要在SDK里写死SDK内部可以保留一个默认的application-sdk.yml或者xxx-sdk.properties用来设置一组合理的默认值。但对外一定要提供清晰的配置项说明让调用方能通过自己工程的application.yml覆盖SDK的默认参数。实现方式用ConfigurationProperties前缀绑定。比如SDK里定义一个类ConfigurationProperties(prefix sdk.payment) public class PaymentSdkProperties { private String appId; private String secretKey; private int connectTimeout 3000; // getter/setter省略 }调用方在自己的配置文件里这样写sdk: payment: app-id: xxxx secret-key: xxxx connect-timeout: 5000这就不会出现SDK的配置写死在代码里导致调用方没法改的情况。3. Maven打包配置的核心实操到了这一步改造工程结构后重点就落在pom.xml的配置上。我把关键操作拆成几步每一步都会说清楚为什么要这么做。3.1 关闭spring-boot-maven-plugin的repackage这是整个打包流程里最核心的一个开关。SpringBoot项目默认的pom里都会有这个插件plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin你不需要完全删掉它只需要不让它执行repackage目标。两种做法方式一声明executions为空plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId executions execution idrepackage/id goals goalnone/goal /goals /execution /executions /plugin方式二给maven-jar-plugin强制指定普通jar的打包方式并显式声明不依赖SpringBoot启动器plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration archive manifest addClasspathfalse/addClasspath /manifest /archive /configuration /plugin个人推荐方式一改动最小语义也清晰后续如果临时要用回可执行jar改回goal值就行。3.2 显式声明依赖范围避免把Web容器带进SDK如果你的SDK模块还保留了Controller相关类那依赖里大概率还挂着spring-boot-starter-web。这个依赖一引入调用方项目会启动内嵌Tomcat产生端口冲突、Session机制变化、异常处理器失效等等不可预知的连锁问题。所以SDK模块的pom里必须做到dependencies !-- 不要引web starter如果需要SpringMVC注解用provided -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId optionaltrue/optional /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId optionaltrue/optional /dependency /dependenciesoptional为true的依赖会正常编译但不会传递给下游。这里是个关键权衡SDK如果过度传递依赖会污染调用方项目搞出一堆版本冲突如果完全不传递调用方又得手动补齐一堆运行时依赖。合理的做法是把直接面向业务的基础库设成optional把核心专用依赖正常传递。比如你做的是一个短信SDK那么阿里云短信SDK、腾讯云SDK这些供应商SDK应该正常传递因为调用方大概率不会自己再用同款SDK但jackson、guava这类通用工具库最好optional因为调用方工程里大概率已经有自己的版本。3.3 使用spring-boot-configuration-processor生成配置元数据如果你提供的SDK里有ConfigurationProperties配置类建议加上这个依赖它会在target目录生成META-INF/spring-configuration-metadata.json让调用方在使用IDE时能获得自动补全和配置提示。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency这个细节看着不大但实际上手体验提升非常明显。没有它的时候调用方配置SDK参数要翻文档对着敲有了它IDE会自动提示每个配置项的含义和默认值降低沟通成本。3.4 阿里云仓库加速依赖下载如果所在网络环境下Maven中央仓库下载慢一定要在settings.xml里配置阿里云镜像仓库不然每次构建都卡在下载依赖上非常耽误事。mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirrorMaven首次使用前需要先做基本配置下载Maven二进制包、解压到指定目录、设置MAVEN_HOME环境变量、配置本地仓库路径。很多新手容易漏掉环境变量配置导致命令行里敲mvn -v不识别。4. 让SDK被调用方自动注册自动配置的三种写法工程搞干净、打包配置改对了还有一个更大的坎在前面SDK里的Bean怎么进到调用方的容器里。SpringBoot默认只会扫描启动类所在包及其子包。如果你的SDK包名是com.xxx.sdk而调用方启动类在com.other.app那你的Bean一个都进不去。这里就要用到SpringBoot的自动配置机制。4.1 使用AutoConfiguration.importsSpringBoot 2.7推荐这是目前最推荐的方式。在SDK模块的resources目录下新建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容一行一个自动配置类com.xxx.sdk.config.PaymentSdkAutoConfiguration注意SpringBoot 3.x里这个路径变了是META-INF/spring不是META-INF。如果看网上老教程用spring.factories那是在SpringBoot 2.7以前的办法新版本虽然仍然兼容但官方已经不推荐了。我平时做新项目直接用AutoConfiguration.imports。4.2 用ConditionalOnMissingBean让调用方可覆盖自动配置类里的Bean定义一定要加上ConditionalOnMissingBean注解给调用方一个覆盖的机会。AutoConfiguration EnableConfigurationProperties(PaymentSdkProperties.class) public class PaymentSdkAutoConfiguration { Bean ConditionalOnMissingBean public PaymentClient paymentClient(PaymentSdkProperties properties) { return new PaymentClient(properties); } }加上这个注解后调用方如果自己定义了PaymentClient这个Bean自动配置就会让位对方的Bean优先生效。这是SDK设计里很重要的一个开放点。没有这个条件的自动配置会把调用方定制的能力堵死到时候对方为了覆盖配置只能把你们的SDK拆开改源码那就失控了。4.3 用EnableXxx注解方式给调用方显式开关自动配置机制虽然省心但缺一个好处调用方有时候希望显式控制。这个时候更推荐做成EnableXxx注解的方式类似mybatis-spring-boot-starter或者EnableScheduling那样。Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Import(PaymentSdkRegistrar.class) public interface EnablePaymentSdk { }调用方在自己配置类上写EnablePaymentSdkSDK就生效不写就不生效。显式、可控、不抢入口。如果团队对接的技术水平参差不齐我觉得这种方式比全自动更稳至少出了问题双方都知道是不是因为没加注解。5. 交付与部署本地仓库、私服Nexus以及调用方引入打包配置和自动配置搞定之后接下来就是把jar交付给对方团队。常见的方式有三种复制jar文件、安装到本地仓库、部署到Nexus私服。最后一种最正规前面两种在小团队里也够用。5.1 复制jar文件直接给这种方式最暴力但简单直接。执行mvn clean package后把target目录下的jar拷给对方让对方手动install到本地仓库mvn install:install-file -Dfileyour-sdk.jar -DgroupIdcom.xxx -DartifactIdxxx-sdk -Dversion1.0.0 -Dpackagingjar这个方式最大的问题是依赖关系容易丢。你的SDK如果依赖了20个第三方库对方只拿到一个jar文件不知道需要补哪些包。虽然可以用Maven生成一个带依赖清单的文件但实际操作中对方团队自己手动补依赖时会很痛苦还容易引入错误版本。所以我只在对方完全不需要传递依赖、SDK是零依赖工具时才这么做。5.2 安装到本地仓库如果两个团队共用一台开发机或者都在同一台CI服务器上构建mvn install就够用。在自己的SDK模块目录下执行mvn clean install这个命令会把jar和pom都安装到本地仓库对方直接正常引坐标就能解析到全链路依赖比复制jar文件高级很多。5.3 部署到Nexus私服正规一点的企业都会有公司的Nexus或者Artifactory私服。SDK的pom里配上发布仓库地址distributionManagement repository idreleases/id urlhttp://nexus.company.com/repository/maven-releases//url /repository snapshotRepository idsnapshots/id urlhttp://nexus.company.com/repository/maven-snapshots//url /snapshotRepository /distributionManagement然后在Maven的settings.xml里配置私服账号servers server idreleases/id usernamedeployer/username passwordyourpassword/password /server /servers执行mvn clean deploy发布完成后对方团队只需要在pom里加一个坐标就能拉到你的SDK所有传递依赖自动解析。这是所有交付方式中最省心的。5.4 调用方引入后会遇到的“版本陷阱”对方拿到你的SDK后真正跑起来还会遇到版本冲突的问题核心在SpringBoot版本上。如果你的SDK是基于SpringBoot 2.7编译的调用方用的是SpringBoot 3.2。SpringBoot 3.2里SpringFramework已经是6.x用到了jakarta命名空间。你的SDK里如果用了javax.servlet.http.HttpServletRequest虽然编译期成功但运行期class找不到对方排查半天也看不出是你SDK的锅因为报错信息里全是一堆框架堆栈。所以做SDK时SpringBoot和SpringFramework版本要刻意控制。我的经验是纯业务逻辑SDK直接用javax标准不依赖SpringBoot的Web组件这样兼容性最好。如果必须带Web相关能力拆出单独的web-sdk包并明确告诉调用方它要求的SpringBoot版本区间。自己SDK里不要引spring-boot-starter-web全家桶用spring-web的编译依赖就够并且optional。版本冲突最典型的现象是jar包明明存在但启动时Bean创建报错堆栈里显示一些类找不到。这类问题排查时第一件事就是用mvn dependency:tree看依赖树哪个版本被仲裁捡起来。90%的冲突都是因为依赖传递链里引到了错误版本不是真的缺jar。6. 常见问题与排查技巧实录把我在实操中积累的常见问题整理成了一份速查表希望能帮省点排查时间。现象原因解决方案对方项目引入后启动失败报NoClassDefFoundError你发的是可执行jar类在BOOT-INF下关闭repackage改打普通jar对方项目启动后出现意外端口或路径冲突SDK里带了Web starter或Controller类从SDK中排除Web依赖移除Controller配置项不生效调用方改了application.yml没用ConfigurationProperties前缀写错或没加EnableConfigurationProperties核对配置前缀确认绑定类被注册Bean找到了多个自动配置重复创建调用方自己也写了同名Bean且配置类没加ConditionalOnMissingBean补上条件注解给调用方覆盖机会对方项目启动缓慢提示大量自动配置匹配失败SDK依赖了太多多余的starter精简依赖能optional就optional自己本地打包正常对方一直解析不到最新版本没执行install/deploy对方用了本地缓存旧坐标mvn clean install重新部署到仓库6.1 打包报错“Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin”这类编译失败这类报错大多不是模式问题而是环境问题。常见原因有三个JDK版本和pom里maven.compiler.source不一致、本机没有配置JAVA_HOME环境变量、Maven仓库里缓存了损坏的依赖。最直接的排查思路是先把项目在干净环境里跑一次确认是不是环境问题。我以前遇到过一种情况本机装了JDK 17但pom里的source/target是1.8编译时报错“invalid target release 17”。改成17之后又发现项目里有几个老依赖不兼容17最终把SpringBoot升到2.7以上才全部编译通过。这里也提醒一句SpringBoot和JDK版本是绑定的SpringBoot 2.x只到JDK 8-17之间能顺畅跑SpringBoot 3.x才要求JDK 17。如果本机装的是JDK 21又建了一个SpringBoot 2.7项目打包时会遇到各种奇怪报错还不好排查。尽量让SpringBoot版本、JDK版本、插件版本三者匹配。6.2 对方项目里提示“无法访问xxx类的jar文件”这个问题的本质是SDK的pom里把某个依赖声明成了provided但调用方没引入。provided作用域在编译期存在运行期不存在如果调用方引用了你的SDK但没引用你provided的那些包就会在运行或编译时找不到类型。解决方案是如果某个依赖编译期确实需要但运行时希望调用方自行管理那就要在SDK文档里明确写清楚“使用本SDK需要在工程中额外引入xx依赖”而不是默认对方一定知道。6.3 依赖树查看技巧调用方引入SDK后检查依赖冲突最有效的命令是mvn dependency:tree加上过滤条件只看某个特定包mvn dependency:tree -Dincludescom.alibaba:fastjson如果发现解析到的fastjson版本不是你期望的可以用exclusion把传递依赖排除掉然后显式指定版本dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.25/version /dependency这个操作容易忽略一个点排除依赖时只排除自己直接能看到的那一层是不够的如果SDK传递依赖里也引了fastjson还要在SDK上游做排除才不会出现绕了一层又重新拉回来的情况。用dependency:tree查两次一次在SDK工程里查一次在调用方工程里查两边一对比就知道哪些路径漏掉了。6.4 调试SDK内部的调试日志SDK发布后调用方集成时经常会说“你的SDK报错了但什么信息都没输出”。这一般是因为SDK内部用了debug级别日志没被调用方的日志级别覆盖。一个好的做法是在SDK里也提供一个开关通过配置项控制内部详细日志sdk: payment: debug: trueSDK内部所有重点链路都打日志通过这个开关控制。调用方遇到问题时打开开关重跑一遍把日志发回来比自己盲猜快得多。这算是一个提升对接效率的小经验。7. 再多说几句关于SDK设计的事技术流程走完还有几个“软实力”层面的东西想分享一下。做SDK不是把代码打包给对方就完事而是要站在调用方的角度想问题。依赖要克制。我见过一个内部SDK为了一个字符串判断功能引了Apache Commons Lang3为了几个JSON处理引了Jackson全家桶再额外引了Guava。调用方项目本身就讲究一个体量控制这些传递依赖要么版本冲突要么臃肿。做SDK时每次引入新依赖前先问一句“真的非要不可吗”能自己写三行代码解决的就别引包。命名要清晰。SDK的groupId、artifactId、版本号要有统一规范最好在公司内部有专门的Maven坐标规范文档。groupId用公司域名反写artifactId用项目功能名版本号遵循SemVer语义化版本规范。这样调用方一看坐标就知道这个SDK是干什么的、哪个版本、有没有破坏性变更。文档要轻。SDK配套的README不是越多越好而是要让调用方在半小时内跑通。我一直采用的模板包含能解决什么问题、Maven坐标、最小可用配置示例、完整配置项对照表、常见问题三到五个、版本变更记录。信息密度够了就行别搞一百多页的技术手册没人看。最后讲一个我在启动件设计时踩过的坑。SpringBoot 2.7之前自动配置注册都在META-INF/spring.factories里写后来我升级SpringBoot到3.0发现自动配置全部失效排查了整整半天才发现是spring.factories不再被识别要迁移到AutoConfiguration.imports。所以做SDK时自动配置文件的兼容性要写清楚免得升级SpringBoot时静默失效一上线就出问题。根据我的经验第一次做SDK对接从开始动手到对方项目里成功调用顺畅一点也要半天时间如果中间踩到repackage、包名、自动配置这几个坑花一两天也不是没可能。把这篇文章里的打包配置思路整理清楚能帮你少走一大半弯路。
返回列表