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
<!-- 阿里云 SDK - Maven 仓库直接获取 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.22</version>
</dependency>

<!-- 腾讯云 SDK - Maven 仓库直接获取 -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.850</version>
</dependency>

亿美短信 SDKeucp-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
<!--  emay 短信 start -->
<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>
<!-- emay 短信 end -->

关键参数说明:

参数 说明
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
// 这些类全部来自外部 JAR: eucp-sms-sdk-1.3.3.jar
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();
// 使用外部 SDK 创建客户端
this.client = new SmsSDKClient(channelDomain, 80, appId, appSecret);
}

@Override
public SmsRespCommand sendSmsByTemplateId(SendSmsReqCommand smsSendRequest) {
// 使用外部 SDK 发送短信
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
<!-- smspro-admin-server/pom.xml -->
<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> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Spring Boot 的 repackage 目标会将所有依赖(包括 system scope)打入最终的可执行 Fat JAR 中。

如果不是 Spring Boot 项目,可以使用 maven-dependency-pluginmaven-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(); // 使用外部 JAR
}
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 的类:

  1. 右键项目 → Maven → Reimport
  2. 检查 systemPath 路径是否正确
  3. 确认 JAR 文件确实存在于指定路径

5.4 CI/CD 环境

在 CI/CD 环境中,由于 system scope 依赖本地文件路径,需要确保:

  • JAR 文件已提交到 Git 仓库(注意 .gitignore 不要排除 *.jar 在 libs 目录下)
  • ${project.basedir} 路径在 CI 环境中能正确解析

六、总结

在 Maven 多模块项目中引入外部 JAR 包,推荐的实践流程为:

  1. 在使用模块内创建 src/libs 目录,存放外部 JAR 文件
  2. pom.xml 中使用 system scope 声明依赖,利用 ${project.basedir} 确保路径可移植
  3. 用适配器模式封装外部 JAR 的调用,避免业务层直接耦合
  4. 配置打包插件(如 spring-boot-maven-pluginrepackage),确保 system scope 的 JAR 包含在最终产物中
  5. 将 JAR 文件纳入版本控制,保证团队成员和 CI/CD 环境的一致性

如果是长期维护的企业项目,建议逐步迁移到私有 Maven 仓库方案,以获得更好的依赖管理体验。


本文基于 platform-sms-pro 短信平台项目的 smspro-module-channel 模块实际代码撰写。


本站由 卡卡龙 使用 Stellar 1.29.1主题创建

本站访问量 次. 本文阅读量 次.