基于 repackage_tools 的快速 Jar 包名修改与常量替换:一键输出马甲包
目标:在不改业务逻辑的前提下,快速完成
1)Jar 内包名(package)/类引用的重命名(repackage)
2)常量(字符串/配置开关/特征值)替换(const replace)
3)**批量输出多份“马甲包”**用于分发、A/B、渠道隔离或策略灰度
1. 背景与诉求拆解
很多场景不希望改源码、重新发版、走完整 CI/CD,只是希望“同一份功能”快速产出多个不同外观的包,例如:
- 渠道包/马甲包:包名、常量、标识不同,但核心逻辑一致
- 快速灰度/对照实验:同功能不同常量(开关、URL、渠道号、策略ID)
- 降低重复劳动:避免每次改一堆
package与常量,再重新打包
这类需求本质上就是两步:
- 重命名包/类路径(确保类引用尽可能同步更新)
- 替换常量特征(比如
APP_ID、CHANNEL、BASE_URL、UA等)
2. 工具能力概览
repackage_tools(包含 JarPackageRenamer.java + ASM 依赖)属于典型的字节码改写方案:
- 包名重命名:将
com.old.sdk.*→com.new.shell.* - 常量替换:替换常量池字符串/编译期常量(如
static final String) - 批量产物输出:一份基础 jar + 多份配置,一键产出多份 jar
相比“字符串粗暴替换 class 文件”,ASM 的优势是:结构化改写、更稳定、更可控。
3. 目录结构与依赖
解压后的结构(示例):
repackage_tools/JarPackageRenamer.javarepackage_tools/asm-commons-9.5.jar
若你的实现还依赖 asm-core/asm-tree 等其他 jar,请补齐依赖到 classpath。
4. 使用方式(推荐做成 CLI:一次编译,多次运行)
说明:不同实现的
main(String[] args)参数可能不同。本文以“推荐的标准参数形式”写法展示:
--in/--out/--from/--to/--replace
你可以按实际参数名替换即可。
4.1 编译工具
在 repackage_tools 目录下:
编译完成会生成 JarPackageRenamer.class。
4.2 单次重命名 Jar 包名
假设:
- 输入 jar:
input.jar - 输出 jar:
output.jar - 旧包名:
com.old.sdk - 新包名:
com.new.shell
复制
java -cp ".:asm-commons-9.5.jar" JarPackageRenamer \
--in input.jar \
--out output.jar \
--from com.old.sdk \
--to com.new.shell
Windows 下 classpath 分隔符是
;:
java -cp ".;asm-commons-9.5.jar" ...
4.3 常量替换(建议用映射文件驱动)
创建 replace.properties:
复制
CHANNEL=huawei
BASE_URL=https://api-a.example.com
APP_ID=10001
运行:
复制
java -cp ".:asm-commons-9.5.jar" JarPackageRenamer \
--in input.jar \
--out output.jar \
--from com.old.sdk \
--to com.new.shell \
--replace replace.properties
5. 一键输出马甲包(批量方案)
核心思路:一份基础 jar + 多份配置 → 多份输出 jar。
5.1 维护马甲配置清单
例如 variants.csv:
复制
name,fromPkg,toPkg,CHANNEL,BASE_URL,APP_ID
v1,com.old.sdk,com.shell.a,huawei,https://api-a.example.com,10001
v2,com.old.sdk,com.shell.b,xiaomi,https://api-b.example.com,10002
v3,com.old.sdk,com.shell.c,oppo,https://api-c.example.com,10003
5.2 脚本批处理(Linux/macOS)
复制
#!/usr/bin/env bash
set -e
INPUT_JAR="input.jar"
CP=".:asm-commons-9.5.jar"
mkdir -p dist
tail -n +2 variants.csv | while IFS=',' read -r name fromPkg toPkg channel baseUrl appId; do
echo "==> build ${name}"
tmp="/tmp/replace_${name}.properties"
cat > "${tmp}" <<EOF
CHANNEL=${channel}
BASE_URL=${baseUrl}
APP_ID=${appId}
EOF
java -cp "${CP}" JarPackageRenamer \
--in "${INPUT_JAR}" \
--out "dist/${name}.jar" \
--from "${fromPkg}" \
--to "${toPkg}" \
--replace "${tmp}"
done
6. 原理与边界(建议原样写进博客)
6.1 为什么用 ASM
- class 文件是结构化二进制,不适合“纯字符串替换”
- ASM 可以在字节码层重写类名、类型签名、方法描述符等
- 重命名 package 时能尽量保证引用一致性
6.2 重命名的边界
ASM 通常能处理:
- 类名、父类、接口
- 字段/方法签名里的类型引用
- 大多数注解中的类型引用
但需要额外关注:
- 反射字符串:
Class.forName("com.old.sdk.Foo") - 资源路径/配置文件:
META-INF/services/*、spring.factories等 - 序列化兼容:类名改变可能影响序列化/反序列化兼容
6.3 常量替换的边界
常量替换对以下最稳:
- 常量池
LDC "xxx"字符串 - 编译期常量(如
public static final String)
不一定覆盖:
- 运行时拼接字符串(不在常量池)
- 混淆/优化后常量被合并或内联(需以最终字节码为准)
7. 常见坑与规避
7.1 Jar 签名失效
如果输入 jar 被签名(META-INF/*.SF, *.RSA, *.DSA),修改后签名一定失效。
建议:输出时删除旧签名文件,必要时重新签名。
7.2 依赖引用不一致
若 A jar 引用了 B jar 的旧包名,只改 A 不改 B 可能 ClassNotFoundException。
建议:同一体系 jar 使用统一映射策略,或仅改你自己的命名空间。
7.3 反射与 SPI
- 反射字符串需纳入替换规则
- SPI(
META-INF/services)也要同步替换 provider 类名
8. 最佳实践(把工具“产品化”)
为了让批量产出稳定可控,建议补齐:
- 参数校验(输入存在、输出目录可写、from/to 合法)
- dry-run(只打印将改写哪些类/多少条常量)
- 统计报告(改写类数量、替换命中次数、未命中 key 列表)
- 可重复构建(同输入同配置产物一致)
9. 总结
通过 JarPackageRenamer + ASM 的字节码重写方式,可以在无需源码的情况下,快速完成 Jar 内包名重命名与常量替换,并通过配置驱动批量输出多个“马甲包”。
该方案稳定、可批处理、易自动化,适合渠道分发、灰度对照等场景;同时需要注意签名失效、反射字符串与资源文件引用等边界问题。
FAQ
Q1:包名改了,为什么运行还找不到类?
常见原因是反射字符串或 SPI/配置文件仍写旧类名。将这些文本资源也纳入替换即可。
Q2:常量替换为什么没生效?
可能是常量被内联/优化、目标字符串非常量池、或替换 key 不一致。建议输出命中统计定位。
Q3:可以对第三方库一起改吗?
不建议全量改第三方。优先只改你自己的命名空间,降低兼容风险。