【日常随笔】如何快速输出 “马甲包jar”?

6 阅读5分钟

基于 repackage_tools 的快速 Jar 包名修改与常量替换:一键输出马甲包

目标:在不改业务逻辑的前提下,快速完成
1)Jar 内包名(package)/类引用的重命名(repackage)
2)常量(字符串/配置开关/特征值)替换(const replace)
3)**批量输出多份“马甲包”**用于分发、A/B、渠道隔离或策略灰度


1. 背景与诉求拆解

很多场景不希望改源码、重新发版、走完整 CI/CD,只是希望“同一份功能”快速产出多个不同外观的包,例如:

  • 渠道包/马甲包:包名、常量、标识不同,但核心逻辑一致
  • 快速灰度/对照实验:同功能不同常量(开关、URL、渠道号、策略ID)
  • 降低重复劳动:避免每次改一堆 package 与常量,再重新打包

这类需求本质上就是两步:

  1. 重命名包/类路径(确保类引用尽可能同步更新)
  2. 替换常量特征(比如 APP_IDCHANNELBASE_URLUA 等)

2. 工具能力概览

repackage_tools(包含 JarPackageRenamer.java + ASM 依赖)属于典型的字节码改写方案

  • 包名重命名:将 com.old.sdk.*com.new.shell.*
  • 常量替换:替换常量池字符串/编译期常量(如 static final String
  • 批量产物输出:一份基础 jar + 多份配置,一键产出多份 jar

相比“字符串粗暴替换 class 文件”,ASM 的优势是:结构化改写、更稳定、更可控


3. 目录结构与依赖

解压后的结构(示例):

  • repackage_tools/JarPackageRenamer.java
  • repackage_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:可以对第三方库一起改吗?
不建议全量改第三方。优先只改你自己的命名空间,降低兼容风险。

附件