SpringBoot项目打包成SDK实战:Maven配置与自动装配全攻略

发布时间:2026/10/2 7:53:11
SpringBoot项目打包成SDK实战:Maven配置与自动装配全攻略 1. 先想清楚为什么要把SpringBoot项目打成SDK先说一个我自己的真实经历。去年我们组做了一个内部的风控规则引擎一开始就是普通SpringBoot服务在自己项目里跑得好好的。后来隔壁交易团队也想用这套规则但他们不想部署一套独立服务更不想把我们的代码拷过去改一改——他们只想在自己的SpringBoot工程里通过依赖坐标引入我们的引擎然后直接Autowired或者new一个客户端类传入参数就能拿到结果。这时候问题就来了我们项目的常规打包产物是一个可执行的fat jar里面内嵌了Tomcat启动方式是java -jar rules-engine.jar。隔壁团队要的是能被他们的Spring容器扫描、能被Maven依赖管理、能直接调用的库而不是一个独立进程。这就是SpringBoot项目打包成SDK和常规打包部署的根本差异。一句话概括SDK的本质是被集成而不是被启动。你要交给别人的不是一坨能跑的进程而是一组精心设计的API、一套清晰的依赖说明、一个在别人容器里能正常工作的类路径代码单元。我最终做下来整条链路涉及的要点其实就三板斧用Maven的maven-source-plugin或maven-jar-plugin把Boot项目改造成可被依赖的普通jar用maven-install-plugin安装到本地仓库或maven-deploy-plugin推到公司私服Nexus/Artifactory处理SpringBoot自动装配、spring.factories或AutoConfiguration.imports、依赖瘦身这些隐藏坑。这篇文章我会按我实际推进的顺序把每一步怎么做、为什么这么做、踩了哪些坑全部写出来。适合的场景包括团队内部组件复用、多项目共享公共能力、公司技术中台沉淀基础服务。不适合的场景我后面也会专门说——比如你其实想要一个可独立部署的微服务那就别打成SDK老老实实做fat jar启动器即可。2. 打包前的设计调整把服务思维切换成SDK思维2.1 先明确SDK的边界对外暴露什么隐藏什么把SpringBoot项目打成SDK第一件要做的不是改pom而是重新审视你的代码结构。日常写服务时我们习惯把事情堆在Controller层HTTP接口就是天然边界。但SDK没有Controller没有HTTP路由它的边界是类和方法是别人在IDE里点.之后能看到的那一串提示。我当时做了这样一个拆分你可以直接套用对外API模块只放接口定义、核心领域模型DTO/VO、必要的枚举和常量。这些是别人要import的必须干净、稳定、文档齐全。内部实现模块放业务逻辑、Repository、第三方client封装、Redis操作等。这些用internal包名或Maven模块隔离原则上不让SDK调用方直接触达。组装层SpringBoot场景下对外暴露一个Configuration自动装配内部实现的Bean或者提供一个静态工厂方法让调用方脱离Spring也能用。比如我那个风控引擎对外暴露的接口长这样public interface RiskEvaluateService { RiskResult evaluate(RiskContext context); }调用方只需要依赖这个接口和RiskContext、RiskResult两个POJO完全不用关心内部用了什么规则脚本、接了什么数据库。这是SDK最容易被忽略但又最重要的设计接口的稳定性决定了你们团队后续的版本发布自由度。2.2 是否保留SpringBoot特性自动装配是双刃剑很多人问我SDK里要不要用SpringBoot的自动装配我的答案是看你的调用方是不是铁定用SpringBoot。如果调用方是SpringBoot项目那自动装配非常爽。你只需要在resources/META-INF下放一个spring.factoriesSpringBoot 2.7版本之前或者AutoConfiguration.importsSpringBoot 2.7开始推荐3.0之后必须用它写清楚你的配置类全限定名对方项目启动时就能自动把Bean注册进去org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.rules.engine.autoconfigure.RiskEngineAutoConfigurationSpringBoot 2.7路径是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容更简洁com.example.rules.engine.autoconfigure.RiskEngineAutoConfiguration但这里有个大坑如果你用了ConditionalOnMissingBean、ConditionalOnProperty这类starter里惯用的条件注解调试起来非常痛苦。对方项目里如果已经有一个同名Bean你的自动装配会静默失效对方配置文件中少了一个前缀配置你的SDK可能给出一个莫名其妙的NullPointerException。所以我的建议是对外核心服务同时提供两种使用方式Spring场景下用自动装配非Spring场景下用静态工厂。自动装配类里必须给出明确的缺省配置值不要让调用方不配置就报错。自动装配类建议用AutoConfiguration注解SpringBoot 2.7来标记比老式Configuration更语义化。一个可以照抄的静态工厂示例public final class RiskEngineSDK { private static volatile RiskEvaluateService instance; public static RiskEvaluateService getInstance() { if (instance null) { synchronized (RiskEngineSDK.class) { if (instance null) { instance new DefaultRiskEvaluateService(); } } } return instance; } }3. Maven打包核心配置三步搞定可被依赖的jar产物3.1 关闭SpringBoot的repackage别让产物变成fat jar这一步是整个打包方案的分水岭。SpringBoot默认的spring-boot-maven-plugin会把你的jar重打包成可执行fat jar里面塞进所有依赖和内嵌容器。但SDK恰恰不需要这个重打包流程——调用方自己的项目会负责依赖管理和启动不需要你替他内置Tomcat。所以pom里要做的是要么不引入spring-boot-maven-plugin要么引入了但显式跳过repackagebuild plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration skiptrue/skip /configuration /plugin /plugins /build注意如果项目本身还需要产出一个可运行的服务jar比如你既要发布SDK又要自己部署一套独立服务那就需要更细的玩法。我的习惯是拆Maven多模块xxx-sdk纯SDK模块不引入spring-boot-maven-plugin产出普通jarxxx-server独立部署模块依赖xxx-sdk引入spring-boot-maven-plugin产出fat jar。这样一套代码两种形态互不干扰。3.2 用maven-jar-plugin控制MANIFEST和类路径既然产物是普通jar我强烈建议配一下maven-jar-plugin至少做两件事明确MANIFEST.MF里不要有Main-Class因为SDK不应该被java -jar启动如果你希望调用方在IDE里依赖时能自动带入其他必要依赖那依赖信息主要靠pom传递而不是塞进jar内部。一个比较完整的配置plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest addDefaultImplementationEntriestrue/addDefaultImplementationEntries /manifest manifestEntries Implementation-Title${project.name}/Implementation-Title Implementation-Version${project.version}/Implementation-Version Built-Byyour-team/Built-By /manifestEntries /archive /configuration /plugin注意addDefaultImplementationEntries会把Implementation-Version写进MANIFEST很多团队会在运行时通过Package.getImplementationVersion()做SDK版本上报有了这个配置就很省事。3.3 同时挂上maven-source-plugin让调用方在IDE里能看源码这是很多SDK不讨人喜欢的原因之一——别人引了你一个jar点进方法想看看注释结果不是Sources not found就是一堆反编译乱码。配上源码插件成本极低收益很高plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-source-plugin/artifactId version3.2.1/version executions execution idattach-sources/id goals goaljar-no-fork/goal /goals /execution /executions /plugin执行mvn install后你的本地仓库里会有xxx-sdk-1.0.0.jar和xxx-sdk-1.0.0-sources.jar两个产物别人在IntelliJ IDEA里点开SDK类就能直接看源码注释遇到问题也能自己排查能省掉你们大量重复答疑时间。4. 依赖瘦身与传递策略该带上的带上不该带的全剪掉4.1 为什么不能把所有依赖都一股脑传过去把SpringBoot项目打成SDK你最大的敌人其实是依赖冲突。你的SDK内部可能用了Jackson、HttpClient、Guava调用方项目里也可能用但版本不同。如果不加控制轻则方法找不到重则启动直接报LinkageError。我踩过最典型的一次SDK里依赖了httpclient 4.5.13调用方项目锁的是httpclient 4.5.3结果他们那边的老代码调CloseableHttpClient的某个新重载方法时运行时NoSuchMethodError排查了整整一天。所以SDK的依赖策略必须遵循几个原则能用JDK自带能力就别引第三方依赖。比如JSON序列化如果调用方是SpringBoot项目那一定已经有Jackson了你的SDK直接以provided或optional方式声明就行。尽量依赖大而全的框架而非零碎小工具。Spring生态里spring-web、spring-context这些调用方几乎必有SDK依赖它们冲突风险很低但像commons-lang3这种版本跨度大的就要斟酌一下。必要时使用optional加上provided把传递依赖降到最低。4.2optional和provided怎么选一张表说清楚看这张表基本就够了依赖声明方式是否传递到调用方典型使用场景默认compile是会传递你SDK独有且必须的逻辑比如内部规则引擎核心optionaltrue否不传递调用方大概率已经有同类依赖或该依赖只是SDK某个可选功能需要provided否不传递容器/框架会提供比如调用方必然有Spring、Servlet APItest否测试专用举例来说我风控引擎SDK的pom依赖如下dependencies !-- 调用方必然有Spring所以用provided -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.29/version scopeprovided/scope /dependency !-- 调用方SpringBoot项目里必有JacksonSDK内部也用它 所以optional防止版本冲突 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.13.5/version optionaltrue/optional /dependency !-- 真正的核心依赖默认传递 -- dependency groupIdorg.mvel/groupId artifactIdmvel2/artifactId version2.4.14.Final/version /dependency /dependencies提示optionaltrue只影响Maven传递依赖不影响你自己编译和打包。也就是说你自己的模块可以正常使用Jackson只是不会强制塞给调用方。4.3 用maven-shade-plugin做有限的依赖合并谨慎使用有一种场景下你还是希望把某些依赖私有化进SDK里你的核心引擎依赖了一个非常小众的库而且这个库的包名和调用方任何依赖都不会冲突但它内部又依赖了别的杂七杂八的东西。这时候可以用maven-shade-plugin把该库的类重新定位relocation到你的命名空间下避免污染调用方类路径。maven-shade-plugin的典型配置plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration createDependencyReducedPomtrue/createDependencyReducedPom relocations relocation patternorg.mvel2/pattern shadedPatterncom.example.rules.engine.internal.org.mvel2/shadedPattern /relocation /relocations artifactSet includes includeorg.mvel:mvel2/include /includes /artifactSet /configuration /execution /executions /plugin不过这里要提醒你shade不是银弹。如果被shade的库通过反射、SPI、Thread.getContextClassLoader()加载资源重新定位后极其容易出诡异问题。我在做规则引擎时shade过MVEL结果MVEL.eval在调用方环境里跑得好好的但一旦对方用了Arthas或者某些字节码增强框架类加载顺序一变就崩。后来我干脆放弃shade直接升级为让对方显式依赖一个fix版本。5. 安装与发布本地仓库、Nexus私服、中央仓库的完整流程5.1 本地验证mvn install到本地仓库先让自己能引用在正式发布前我每次都会先在本地仓库做一次安装验证。步骤非常简单mvn clean install -DskipTests这条命令会把xxx-sdk-1.0.0.jar安装到本地~/.m2/repository下。然后我在一个全新的测试工程里添加如下依赖dependency groupIdcom.example/groupId artifactIdrules-engine-sdk/artifactId version1.0.0/version /dependency如果这个测试工程能正常编译、启动、调用SDK接口才说明依赖传递没问题。我在这一步经常发现一些只有别人视角才会踩到的问题比如某个内部类因为没写public导致调用方编译失败或者某个POJO的getter/setter缺失导致Jackson序列化异常。提示本地验证时建议用mvn dependency:tree检查一下SDK的传递依赖树看看有哪些意料之外的依赖被带过去了。如果发现某个依赖不该出现回到pom里把它改成optional或provided。5.2 推到Nexus私服mvn deploy与仓库配置本地验证通过之后SDK要提供给外部团队就需要推到公司内部私服。以Nexus为例配置分两部分。第一部分是settings.xml里的server认证信息servers server idnexus-releases/id usernamedeploy-user/username passworddeploy-password/password /server server idnexus-snapshots/id usernamedeploy-user/username passworddeploy-password/password /server /servers第二部分是pom里的distributionManagementdistributionManagement repository idnexus-releases/id urlhttps://nexus.example.com/repository/maven-releases//url /repository snapshotRepository idnexus-snapshots/id urlhttps://nexus.example.com/repository/maven-snapshots//url /snapshotRepository /distributionManagement然后执行mvn clean deploy -DskipTests这里有个版本号策略问题。如果你发布的版本号带-SNAPSHOT后缀例如1.0.0-SNAPSHOTMaven会推到snapshotRepository不带后缀则推到正式repository。同版本号的SNAPSHOT可以反复覆盖发布但正式版本一旦发布就不建议再覆盖因为其他团队可能已经基于这个版本构建了应用你覆盖后他们本地缓存不刷新会出现我明明改了别人怎么还是旧行为的经典困惑。5.3 中央仓库发布开源场景需要准备的额外材料如果这个SDK是开源项目要发布到Maven Central那流程就重一些。按照Sonatype的规范你需要一个独立的groupId域名所有权证明一般用GitHub Pages挂个POM验证文件gpg签名Maven插件在deploy时用你的私钥对文件签名在~/.m2/settings.xml里配置ossrh账号和gpg passphrase在pom里补充licenses、developers、scm等元数据。开源发布的细节比较多这里不展开全部配置但提一个核心点如果你想开源一个SpringBoot风格的SDK务必先确认你的自动装配方式是否支持SpringBoot 3.x和Jakarta命名空间。如果你内部用了javax.annotation等老包SpringBoot 3项目引入后启动大概率报ClassNotFoundException。这不是打不打包的问题而是SDK基线版本兼容的问题。6. SpringBoot自动装配的坑与排查方法6.1 自动装配不生效常见的五个原因把SDK做好之后最常收到调用方的反馈就是我引入依赖了怎么没有Bean。根据我的经验自动装配不生效的原因集中在五类AutoConfiguration.imports文件路径放错。SpringBoot 2.7必须放在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports注意文件名是AutoConfiguration的复数形式写错了就不会被加载。spring.factories文件里配置的键不对。2.7之前用org.springframework.boot.autoconfigure.EnableAutoConfiguration而不是org.springframework.boot.autoconfigure.AutoConfiguration很多人会把这俩搞混。配置类没有被扫描到。如果你的SDK配置类放到了类似com.example.sdk.internal.config这样的包但调用方启动类扫描的是com.other.app那即使自动装配没配置好Spring也扫不到你的Bean。条件注解不满足。最常见的是ConditionalOnMissingBean误伤——调用方项目里恰好有一个同名Bean你的就被跳过了。SDK的自动装配类被SpringBoot的exclude排除掉了。有些团队为了启动速度会排除掉一批AutoConfiguration如果你的SDK在名单里自然失效。排查方法其实很固定在调用方配置文件里加一个调试开关看启动日志里有没有类似下面的AutoConfigurationReport记录debugtrue然后搜你自己的SDK类名看它是matched还是negative match从而快速定位是不是条件注解问题。6.2 配置属性的设计与校验不要等到运行时才报错一个成熟的SDK应当提供显式的配置项并通过ConfigurationProperties绑定。举例ConfigurationProperties(prefix rules.engine) public class RiskEngineProperties { /** * 规则文件路径支持classpath:前缀 */ private String ruleLocation classpath:rules/default-rule.mvel; /** * 白名单模式true只执行白名单规则false执行全部规则 */ private boolean whitelistOnly false; public String getRuleLocation() { return ruleLocation; } public void setRuleLocation(String ruleLocation) { this.ruleLocation ruleLocation; } public boolean isWhitelistOnly() { return whitelistOnly; } public void setWhitelistOnly(boolean whitelistOnly) { this.whitelistOnly whitelistOnly; } }然后在自动装配类里通过EnableConfigurationProperties(RiskEngineProperties.class)激活并在Bean初始化方法中做参数校验。这里有一个我很想强调的教训SDK的配置项默认值必须保守且可用宁可让调用方拿到一个能用但不优化的默认行为也不要让他缺一项配置就启动失败。如果你确实需要某项必填配置就在afterPropertiesSet()或PostConstruct里抛一个IllegalArgumentException错误消息里把缺失项、预期格式、示例值都写清楚。我见过太多SDK在缺配置时只是静默地用一个null值结果调用方在业务高峰期才炸出一个NPE。6.3 与调用方互相覆盖Bean的终极方案AutoConfiguration.after如果SDK和调用方确实存在同名Bean或同类型Bean冲突SpringBoot 2.7提供了AutoConfiguration(after SomeOtherAutoConfiguration.class)这样一个快捷方式用来控制多个自动配置类的加载顺序。但同名Bean的冲突本质上是无法通过顺序解决的你必须在设计上规避对外部使用ConditionalOnMissingBean让调用方可以覆盖你的实现如果你的SDK需要覆盖调用方已有的某个Bean最好别做这种霸道SDK设计而是额外提供一个新的Bean名称让调用方显式选择用哪个。7. 多环境与版本管理SDK发布后怎么持续迭代7.1 版本号规范Semver是一个简单的约定但值得严格执行SDK一旦被多个团队引用版本的语义就必须清晰。我采用的是语义化版本MAJOR.MINOR.PATCHMAJOR大版本不兼容的API变更。比如改了接口方法签名、删除了某个公开类。这种情况下调用方必须改代码务必在发版说明里写明迁移步骤。MINOR小版本向后兼容的新功能。新增一个接口方法、新增一个配置项等。PATCH修订版本向后兼容的Bug修复。不该有任何行为变更。如果你发布的是1.2.0-SNAPSHOT那意味着还在迭代中随时可能变化别人不应该在正式环境依赖SNAPSHOT版本。这一点必须在团队规范里写死。7.2 兼容性测试SDK只有在别人的环境里测过才算数这里说的兼容性测试不是你自己项目的单元测试而是站在调用方的视角做集成测试。我在发布前会准备至少两个不同版本的SpringBoot调用方工程一个SpringBoot 2.7.x的测试工程一个SpringBoot 3.2.x的测试工程一个纯Spring无SpringBoot的测试工程。然后在每个工程里执行相同的调用mvn clean install时把这个测试工程也纳入CI用mvn test跑一遍冒烟用例确保SDK在这几种环境里都能正常工作。之所以强调这点是因为我在SpringBoot 3.0刚出来时吃过亏我那个SDK里的内部校验逻辑用了javax.validationSpringBoot 2.x下没问题SpringBoot 3.x改成了jakarta.validation调用方项目直接启动失败。后面我在SDK的compile依赖里把javax.validation彻底去掉改为使用自研的轻量校验才真正兼容了两种体系。7.3 API兼容性防护用revapi或japicmp盯住公开APISDK发布后最怕的是某个开发在重构时顺手改了一个公开方法的参数类型还自认为反正实现变了调用方也没人用。等对方团队上线才发现那已经晚了。我的做法是在CI里加上japicmp插件专门比对当前分支和上一个发布版本的公开API差异plugin groupIdcom.github.siom79.japicmp/groupId artifactIdjapicmp-maven-plugin/artifactId version0.17.2/version configuration oldVersion dependency groupIdcom.example/groupId artifactIdrules-engine-sdk/artifactId version1.1.0/version /dependency /oldVersion newVersion file path${project.build.directory}/${project.build.finalName}.jar/path /file /newVersion breakBuildOnModificationstrue/breakBuildOnModifications /configuration /plugin这样一旦有破坏性API变更构建直接失败倒逼开发人员正视版本号的升级。初期会有点烦但坚持下来之后SDK的接口稳定性能提升一个档次。8. 实战中的其他常见问题与解决记录8.1 调用方是普通Java工程而不是SpringBoot工程有些团队可能还没升级到SpringBoot或者他们本身就是比较轻量的纯Java应用。这种情况下你的自动装配机制就完全没用了。解决方案是在SDK中保留一个不依赖Spring的入口比如前面提到的静态工厂RiskEngineSDK.getInstance()。同时要确保你的SDK里没有在类加载时就初始化Spring相关Bean。这里有一个隐藏坑如果你在某个类的静态代码块里用ClassPathXmlApplicationContext去读Spring配置那纯Java调用方一加载这个类就会抛异常。所以所有Spring上下文相关操作必须延迟到Bean实例化时再触发而不是类加载阶段。8.2 调用方编译报错Cannot resolve symbol但jar确实在这种问题一般都出在依赖坐标对不上。典型的排查路径是检查groupId、artifactId、version是否和发布时完全一致用mvn dependency:get手动拉取该坐标看仓库里实际是否存在如果拉取的是旧版本检查Nexus里是否同时存在maven-releases和maven-snapshots两种仓库调用方可能配置了错误的repository id。有一个很隐蔽的坑如果你在本地mvn install过和私服上相同版本号的jarMaven本地仓库优先会命中本地。如果本地是旧代码你怎么pull私服都没用。这种情况直接在IDE里删掉本地~/.m2/repository/com/example/xxx目录重新reimport即可。8.3 日志冲突SDK内部要不要打日志怎么打SDK内部肯定需要日志但最好不要直接用System.out.println也不要强依赖某个具体的日志实现。我的实践是引入org.slf4j:slf4j-api作用域设为provided不捆绑logback-classic或log4j2实现让调用方自己的日志框架来决定输出位置日志内容要包含SDK版本号和可追踪的请求ID方便对方出问题时把日志发给你排查。例如private static final Logger log LoggerFactory.getLogger(DefaultRiskEvaluateService.class); public RiskResult evaluate(RiskContext context) { String traceId context.getTraceId(); log.info([rules-engine-sdk:{}] evaluate start, traceId{}, VersionHolder.getVersion(), traceId); try { RiskResult result doEvaluate(context); log.info([rules-engine-sdk:{}] evaluate success, traceId{}, VersionHolder.getVersion(), traceId); return result; } catch (Exception e) { log.error([rules-engine-sdk:{}] evaluate error, traceId{}, ruleLocation{}, VersionHolder.getVersion(), traceId, props.getRuleLocation(), e); throw new RiskEngineException(evaluate failed, e); } }这样好的日志设计能省掉大量跨团队排查问题的时间。8.4 调用方用spring-boot-devtools导致SDK里Bean被重复初始化这个坑比较冷门但很恶心。如果调用方在开发阶段开了spring-boot-devtools它的重启restart机制会使用不同的ClassLoader加载依赖。如果你的SDK里有静态单例可能会出现同一个SDK代码被两个类加载器加载静态变量被初始化两次的问题。更严重的是如果你的自动装配类里用Thread.currentThread().getContextClassLoader().getResources(rules/xxx.rule)查找资源在devtools的环境下可能找到两份资源。我的解决办法是资源加载统一用class.getResourceAsStream或Spring的ResourcePatternResolver而不是裸用Thread上下文ClassLoader。9. 一套可直接抄的完整pom参考这里放一个完整的、个人项目验证过的SDKpom.xml骨架你可以直接改坐标后使用?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdrules-engine-sdk/artifactId version1.2.0/version packagingjar/packaging namerules-engine-sdk/name description风控规则引擎SDK供各业务团队快速集成规则判定能力/description properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-autoconfigure/artifactId version2.7.18/version scopeprovided/scope /dependency dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.29/version scopeprovided/scope /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.13.5/version optionaltrue/optional /dependency dependency groupIdorg.slf4j/groupId artifactIdslf4j-api/artifactId version1.7.36/version scopeprovided/scope /dependency !-- 实际业务核心依赖 -- dependency groupIdorg.mvel/groupId artifactIdmvel2/artifactId version2.4.14.Final/version /dependency dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependency /dependencies build plugins !-- 不能有spring-boot-maven-plugin的repackage保持普通jar -- !-- jar插件: 写入版本信息等MANIFEST -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest addDefaultImplementationEntriestrue/addDefaultImplementationEntries /manifest manifestEntries Implementation-Title${project.name}/Implementation-Title Implementation-Version${project.version}/Implementation-Version /manifestEntries /archive /configuration /plugin !-- 源码jar -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-source-plugin/artifactId version3.2.1/version executions execution idattach-sources/id goals goaljar-no-fork/goal /goals /execution /executions /plugin /plugins /build distributionManagement repository idnexus-releases/id urlhttps://nexus.example.com/repository/maven-releases//url /repository snapshotRepository idnexus-snapshots/id urlhttps://nexus.example.com/repository/maven-snapshots//url /snapshotRepository /distributionManagement /project这个pom的特点核心依赖只保留一个mvel2作为传递依赖所有调用方环境里大概率已有的依赖全部provided或optional构建产物同时产出普通jar和源码jar不引入spring-boot-maven-plugin默认产物就是普通可被依赖的jar。10. 由工具到机制让SDK变成团队公共资产把SpringBoot项目打成SDK这件事技术上其实不难真正难的是以SDK的思维去维护它。很多人第一次这么做时只学会了一堆Maven插件配置但不知道版本号、接口兼容性、自动装配边界、依赖策略这些才是SDK是否好用的关键。我个人在实际落地中的体会是一个SDK被其他团队使用之后你就不是在开发自己项目里的一个模块而是在运营一款面向内部用户的产品。你需要写清楚使用文档、给出快速开始的demo工程、及时记录不兼容变更、主动维护一个面向调用方的变更日志CHANGELOG。如果你要推给多个团队还可以在私服上建一个maven-public代理组把第三方中央仓库、自己公司的release仓库、snapshot仓库都聚合到一个URL里这样调用方只需要配一个repository地址不用在镜像切来切去。这一步看似简单却能帮调用方省下大量的依赖拉取困扰也是我从只做SDK到做平台能力之间的一个转折点。最后再分享一个小技巧SDK里不要写任何依赖具体业务方的包名或类名。哪怕是做一个ConditionalOnClass(name com.some.team.SomeClass)的判断也最好避免。因为一旦调用方重构了包名你的SDK虽然没直接引用它但条件判断里的字符串也会失效。让SDK保持对业务方一无所知它才能被越多的团队放心使用。