Maven 项目中引入外部 JAR 包的实践指南 —— 以短信平台集成亿美 SDK 为例
前言
在 Java 项目开发中,绝大多数依赖都可以通过 Maven 中央仓库或私服获取。但在实际业务中,我们经常会遇到第三方厂商只提供 JAR 包而未上传到 Maven 仓库的情况。例如:短信服务商的 SDK、硬件设备厂商的驱动包、内部未发布的工具包等。
本文以 platform-sms-pro 短信平台中集成亿美(Emay)短信 SDK 的真实案例,详细介绍如何在 Maven 多模块项目中优雅地引入外部 JAR 包。
一、背景与挑战
在短信平台项目中,我们需要对接多家短信服务商(阿里云、腾讯云、亿美等)。阿里云和腾讯云的 SDK 都可以直接从 Maven 中央仓库获取:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>com.aliyun</groupId> <artifactId>dysmsapi20170525</artifactId> <version>2.0.22</version> </dependency>
<dependency> <groupId>com.tencentcloudapi</groupId> <artifactId>tencentcloud-sdk-java</artifactId> <version>3.1.850</version> </dependency>
|
但亿美短信 SDK(eucp-sms-sdk-1.3.3.jar)并未发布到 Maven 仓库,只提供了一个 JAR 文件下载。这就需要我们用特殊方式将其引入项目。
二、实现方案:Maven System Scope
2.1 整体思路
采用 Maven 的 system 作用域(scope) 机制,将外部 JAR 包存放在项目模块内部,通过 systemPath 指定本地路径。
核心步骤如下:
1 2 3 4 5 6
| ┌─────────────────────────────────────────┐ │ 1. 在模块中创建 libs 目录存放 JAR 文件 │ │ 2. 在 pom.xml 中声明 system scope 依赖 │ │ 3. 在代码中正常 import 使用 │ │ 4. 配置打包插件确保 JAR 包含在最终产物中 │ └─────────────────────────────────────────┘
|
2.2 目录结构
1 2 3 4 5 6 7 8 9 10 11 12
| smspro-module-channel/ ├── src/ │ ├── libs/ ← 存放外部 JAR 包 │ │ └── eucp-sms-sdk-1.3.3.jar ← 亿美短信 SDK │ └── main/ │ ├── java/ │ │ └── cn/javayong/smspro/module/smschannel/ │ │ └── adapter/ │ │ └── emay/ │ │ └── EmaySmsOuterAdapter.java ← 使用外部 JAR 的代码 │ └── resources/ └── pom.xml ← 声明 system scope 依赖
|
为什么放在 src/libs 而不是项目根目录?
将 JAR 放在对应模块的 src/libs 下,遵循了模块内聚原则。谁用谁管理,避免多模块项目中的路径混乱。
2.3 pom.xml 配置
在 smspro-module-channel/pom.xml 中,通过 system scope 声明依赖:
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>cn.emay</groupId> <artifactId>eucp-sms-sdk</artifactId> <scope>system</scope> <version>1.3.3</version> <systemPath>${project.basedir}/src/libs/eucp-sms-sdk-1.3.3.jar</systemPath> </dependency>
|
关键参数说明:
| 参数 |
说明 |
groupId |
自定义填写,建议与 JAR 包的实际包名一致 |
artifactId |
自定义填写,建议与 JAR 文件名对应 |
version |
自定义填写,建议与 JAR 实际版本一致 |
scope |
**必须设置为 system**,表示依赖来自本地文件系统 |
systemPath |
JAR 文件的绝对路径,使用 ${project.basedir} 变量确保路径可移植 |
${project.basedir} 是 Maven 内置变量,指向当前模块的根目录,确保不同开发者 checkout 后路径依然正确。
2.4 代码中正常使用
配置完成后,IDE 和编译器都能正确识别外部 JAR 中的类,可以像使用普通依赖一样 import:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import cn.emay.sdk.client.SmsSDKClient; import cn.emay.sdk.core.dto.sms.common.ResultModel; import cn.emay.sdk.core.dto.sms.request.SmsSingleRequest; import cn.emay.sdk.core.dto.sms.response.SmsResponse; import cn.emay.sdk.util.HttpUtil;
public class EmaySmsOuterAdapter implements SmsOuterAdapter {
private SmsSDKClient client;
@Override public void init(SmsChannelConfig smsChannelConfig) throws Exception { String channelDomain = smsChannelConfig.getChannelDomain(); String appId = smsChannelConfig.getChannelAppkey(); String appSecret = smsChannelConfig.getChannelAppsecret(); this.client = new SmsSDKClient(channelDomain, 80, appId, appSecret); }
@Override public SmsRespCommand sendSmsByTemplateId(SendSmsReqCommand smsSendRequest) { SmsSingleRequest request = new SmsSingleRequest( mobile, content, customSmsId, extendedCode, "" ); ResultModel<SmsResponse> result = client.sendSingleSms(request); if ("SUCCESS".equals(result.getCode())) { return new SmsRespCommand(SmsRespCommand.SUCCESS_CODE, result.getResult().getSmsId()); } return new SmsRespCommand(SmsRespCommand.FAIL_CODE); } }
|
2.5 打包配置
system scope 的依赖默认不会被 Maven 打入最终 JAR/WAR 包。在 Spring Boot 项目中,需要在启动模块的 spring-boot-maven-plugin 中配置 repackage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
|
Spring Boot 的 repackage 目标会将所有依赖(包括 system scope)打入最终的可执行 Fat JAR 中。
如果不是 Spring Boot 项目,可以使用 maven-dependency-plugin 或 maven-assembly-plugin 来实现类似效果。
三、适配器模式封装
在本项目中,外部 JAR 的使用被封装在适配器模式中,这是一种推荐的实践:
1 2 3 4 5 6 7 8 9 10
| ┌─────────────────────────┐ │ SmsOuterAdapter (接口) │ └────────────┬────────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ┌───────────┴──────────┐ ┌──────────┴──────────┐ ┌──────────┴──────────┐ │ AliyunSmsOuterAdapter│ │ TencentSmsOuterAdapter│ │ EmaySmsOuterAdapter │ │ (Maven 仓库依赖) │ │ (Maven 仓库依赖) │ │ (外部 JAR 依赖) │ └──────────────────────┘ └──────────────────────┘ └──────────────────────┘
|
工厂类根据渠道编码创建对应的适配器实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class SmsAdapterFactory {
public static SmsOuterAdapter createOuterAdapter(String channelCode, SmsChannelConfig config) throws Exception { SmsOuterAdapter smsOuterAdapter = null; if (StringUtils.equals(channelCode, "aliyun")) { smsOuterAdapter = new AliyunSmsOuterAdapter(); } if (StringUtils.equals(channelCode, "tencent")) { smsOuterAdapter = new TencentSmsOuterAdapter(); } if (StringUtils.equals(channelCode, "emay")) { smsOuterAdapter = new EmaySmsOuterAdapter(); } if (smsOuterAdapter != null) { smsOuterAdapter.init(config); } return smsOuterAdapter; } }
|
这样做的好处是:外部 JAR 的使用被限制在特定的适配器类中,业务层完全无感知,降低了耦合度。
四、其他引入外部 JAR 的方式对比
除了 system scope,还有几种常见的引入方式:
方式一:安装到本地仓库(mvn install:install-file)
1 2 3 4 5 6
| mvn install:install-file \ -Dfile=eucp-sms-sdk-1.3.3.jar \ -DgroupId=cn.emay \ -DartifactId=eucp-sms-sdk \ -Dversion=1.3.3 \ -Dpackaging=jar
|
然后按正常依赖使用即可:
1 2 3 4 5
| <dependency> <groupId>cn.emay</groupId> <artifactId>eucp-sms-sdk</artifactId> <version>1.3.3</version> </dependency>
|
方式二:部署到私有 Maven 仓库
1 2 3 4 5 6 7 8
| mvn deploy:deploy-file \ -Dfile=eucp-sms-sdk-1.3.3.jar \ -DgroupId=cn.emay \ -DartifactId=eucp-sms-sdk \ -Dversion=1.3.3 \ -Dpackaging=jar \ -Durl=http://your-nexus/repository/third-party/ \ -DrepositoryId=nexus-releases
|
方式三:项目内 Maven 仓库
在项目中创建一个本地 Maven 仓库目录结构:
1 2 3 4
| project-repo/ └── cn/emay/eucp-sms-sdk/1.3.3/ ├── eucp-sms-sdk-1.3.3.jar └── eucp-sms-sdk-1.3.3.pom
|
然后在 pom.xml 中声明本地仓库:
1 2 3 4 5 6
| <repositories> <repository> <id>project-repo</id> <url>file://${project.basedir}/project-repo</url> </repository> </repositories>
|
各方式对比
| 方式 |
优点 |
缺点 |
适用场景 |
| system scope |
简单直接,JAR 随项目走 |
Maven 官方不推荐,可能有打包问题 |
快速集成,JAR 数量少 |
| install-file |
使用标准 Maven 方式 |
每个开发者都要执行安装命令 |
个人开发,临时使用 |
| 私有仓库 |
团队共享,最规范 |
需要搭建 Nexus/Artifactory |
企业级团队开发 |
| 项目内仓库 |
JAR 随项目走,且符合 Maven 规范 |
目录结构较复杂 |
中小团队,需要 Git 管理 |
五、注意事项与踩坑记录
5.1 system scope 的限制
system scope 的依赖不会传递。如果模块 A 依赖了 system scope 的 JAR,模块 B 依赖模块 A 时,不会自动获得该 JAR。
systemPath 必须是绝对路径,建议使用 ${project.basedir} 开头确保可移植性。
5.2 打包时 JAR 未包含
最常见的问题是打成可执行 JAR 后运行报 ClassNotFoundException。解决方案:
- Spring Boot 项目:确保
spring-boot-maven-plugin 配置了 repackage goal。
- 普通 Maven 项目:使用
maven-assembly-plugin 或在 maven-jar-plugin 中配置 Class-Path。
5.3 IDE 无法识别
如果 IDEA 无法识别 system scope 的类:
- 右键项目 → Maven → Reimport
- 检查
systemPath 路径是否正确
- 确认 JAR 文件确实存在于指定路径
5.4 CI/CD 环境
在 CI/CD 环境中,由于 system scope 依赖本地文件路径,需要确保:
- JAR 文件已提交到 Git 仓库(注意
.gitignore 不要排除 *.jar 在 libs 目录下)
${project.basedir} 路径在 CI 环境中能正确解析
六、总结
在 Maven 多模块项目中引入外部 JAR 包,推荐的实践流程为:
- 在使用模块内创建
src/libs 目录,存放外部 JAR 文件
- 在
pom.xml 中使用 system scope 声明依赖,利用 ${project.basedir} 确保路径可移植
- 用适配器模式封装外部 JAR 的调用,避免业务层直接耦合
- 配置打包插件(如
spring-boot-maven-plugin 的 repackage),确保 system scope 的 JAR 包含在最终产物中
- 将 JAR 文件纳入版本控制,保证团队成员和 CI/CD 环境的一致性
如果是长期维护的企业项目,建议逐步迁移到私有 Maven 仓库方案,以获得更好的依赖管理体验。
本文基于 platform-sms-pro 短信平台项目的 smspro-module-channel 模块实际代码撰写。