☰
SpringBoot项目打包成SDK实战:Maven配置与自动装配全攻略
2026/10/2 7:53:00 网站建设 项目流程

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.factories(SpringBoot 2.7版本之前)或者AutoConfiguration.imports(SpringBoot 2.7开始推荐,3.0之后必须用它),写清楚你的配置类全限定名,对方项目启动时就能自动把Bean注册进去:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.rules.engine.autoconfigure.RiskEngineAutoConfiguration

SpringBoot 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,要么引入了但显式跳过repackage:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build>

注意:如果项目本身还需要产出一个可运行的服务jar(比如你既要发布SDK,又要自己部署一套独立服务),那就需要更细的玩法。我的习惯是拆Maven多模块:

  • xxx-sdk:纯SDK模块,不引入spring-boot-maven-plugin,产出普通jar;
  • xxx-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> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> </manifest> <manifestEntries> <Implementation-Title>${project.name}</Implementation-Title> <Implementation-Version>${project.version}</Implementation-Version> <Built-By>your-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> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar-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独有且必须的逻辑:比如内部规则引擎核心
optional=true否,不传递调用方大概率已经有同类依赖,或该依赖只是SDK某个可选功能需要
provided否,不传递容器/框架会提供:比如调用方必然有Spring、Servlet API
test否测试专用

举例来说,我风控引擎SDK的pom依赖如下:

<dependencies> <!-- 调用方必然有Spring,所以用provided --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.29</version> <scope>provided</scope> </dependency> <!-- 调用方SpringBoot项目里必有Jackson,SDK内部也用它, 所以optional防止版本冲突 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.5</version> <optional>true</optional> </dependency> <!-- 真正的核心依赖,默认传递 --> <dependency> <groupId>org.mvel</groupId> <artifactId>mvel2</artifactId> <version>2.4.14.Final</version> </dependency> </dependencies>

提示:optional=true只影响Maven传递依赖,不影响你自己编译和打包。也就是说,你自己的模块可以正常使用Jackson,只是不会强制塞给调用方。

4.3 用maven-shade-plugin做有限的依赖合并(谨慎使用)

有一种场景下你还是希望把某些依赖"私有化"进SDK里:你的核心引擎依赖了一个非常小众的库,而且这个库的包名和调用方任何依赖都不会冲突,但它内部又依赖了别的杂七杂八的东西。这时候可以用maven-shade-plugin把该库的类重新定位(relocation)到你的命名空间下,避免污染调用方类路径。

maven-shade-plugin的典型配置:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <createDependencyReducedPom>true</createDependencyReducedPom> <relocations> <relocation> <pattern>org.mvel2</pattern> <shadedPattern>com.example.rules.engine.internal.org.mvel2</shadedPattern> </relocation> </relocations> <artifactSet> <includes> <include>org.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> <groupId>com.example</groupId> <artifactId>rules-engine-sdk</artifactId> <version>1.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> <id>nexus-releases</id> <username>deploy-user</username> <password>deploy-password</password> </server> <server> <id>nexus-snapshots</id> <username>deploy-user</username> <password>deploy-password</password> </server> </servers>

第二部分是pom里的distributionManagement:

<distributionManagement> <repository> <id>nexus-releases</id> <url>https://nexus.example.com/repository/maven-releases/</url> </repository> <snapshotRepository> <id>nexus-snapshots</id> <url>https://nexus.example.com/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement>

然后执行:

mvn clean deploy -DskipTests

这里有个版本号策略问题。如果你发布的版本号带-SNAPSHOT后缀,例如1.0.0-SNAPSHOT,Maven会推到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?"。根据我的经验,自动装配不生效的原因集中在五类:

  1. AutoConfiguration.imports文件路径放错。SpringBoot 2.7+必须放在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,注意文件名是AutoConfiguration的复数形式,写错了就不会被加载。
  2. spring.factories文件里配置的键不对。2.7之前用org.springframework.boot.autoconfigure.EnableAutoConfiguration,而不是org.springframework.boot.autoconfigure.AutoConfiguration,很多人会把这俩搞混。
  3. 配置类没有被扫描到。如果你的SDK配置类放到了类似com.example.sdk.internal.config这样的包,但调用方启动类扫描的是com.other.app,那即使自动装配没配置好,Spring也扫不到你的Bean。
  4. 条件注解不满足。最常见的是@ConditionalOnMissingBean误伤——调用方项目里恰好有一个同名Bean,你的就被跳过了。
  5. SDK的自动装配类被SpringBoot的exclude排除掉了。有些团队为了启动速度会排除掉一批AutoConfiguration,如果你的SDK在名单里,自然失效。

排查方法其实很固定:在调用方配置文件里加一个调试开关,看启动日志里有没有类似下面的AutoConfigurationReport记录:

debug=true

然后搜你自己的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.PATCH:

  • MAJOR(大版本):不兼容的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.validation,SpringBoot 2.x下没问题,SpringBoot 3.x改成了jakarta.validation,调用方项目直接启动失败。后面我在SDK的compile依赖里把javax.validation彻底去掉,改为使用自研的轻量校验,才真正兼容了两种体系。

7.3 API兼容性防护:用revapi或japicmp盯住公开API

SDK发布后,最怕的是某个开发在重构时顺手改了一个公开方法的参数类型,还自认为"反正实现变了,调用方也没人用"。等对方团队上线才发现,那已经晚了。

我的做法是在CI里加上japicmp插件,专门比对当前分支和上一个发布版本的公开API差异:

<plugin> <groupId>com.github.siom79.japicmp</groupId> <artifactId>japicmp-maven-plugin</artifactId> <version>0.17.2</version> <configuration> <oldVersion> <dependency> <groupId>com.example</groupId> <artifactId>rules-engine-sdk</artifactId> <version>1.1.0</version> </dependency> </oldVersion> <newVersion> <file> <path>${project.build.directory}/${project.build.finalName}.jar</path> </file> </newVersion> <breakBuildOnModifications>true</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确实在

这种问题一般都出在依赖坐标对不上。典型的排查路径是:

  1. 检查groupId、artifactId、version是否和发布时完全一致;
  2. 用mvn dependency:get手动拉取该坐标,看仓库里实际是否存在;
  3. 如果拉取的是旧版本,检查Nexus里是否同时存在maven-releases和maven-snapshots两种仓库,调用方可能配置了错误的repository id。

有一个很隐蔽的坑:如果你在本地mvn install过和私服上相同版本号的jar,Maven本地仓库优先会命中本地。如果本地是旧代码,你怎么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 version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>rules-engine-sdk</artifactId> <version>1.2.0</version> <packaging>jar</packaging> <name>rules-engine-sdk</name> <description>风控规则引擎SDK,供各业务团队快速集成规则判定能力</description> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.7.18</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.29</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.5</version> <optional>true</optional> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> <scope>provided</scope> </dependency> <!-- 实际业务核心依赖 --> <dependency> <groupId>org.mvel</groupId> <artifactId>mvel2</artifactId> <version>2.4.14.Final</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- 不能有spring-boot-maven-plugin的repackage,保持普通jar --> <!-- jar插件: 写入版本信息等MANIFEST --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> </manifest> <manifestEntries> <Implementation-Title>${project.name}</Implementation-Title> <Implementation-Version>${project.version}</Implementation-Version> </manifestEntries> </archive> </configuration> </plugin> <!-- 源码jar --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.2.1</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <distributionManagement> <repository> <id>nexus-releases</id> <url>https://nexus.example.com/repository/maven-releases/</url> </repository> <snapshotRepository> <id>nexus-snapshots</id> <url>https://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保持"对业务方一无所知",它才能被越多的团队放心使用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询