使用javassist,修改jar包方法实现
文章目录
本篇仅用于记录本人学习javassist
的过程, 其中任何步骤或思想被用于非法用途与本人无关
环境介绍
- macOS
- Jdk8, 因为我是安卓开发, 事实上理论上也支持 14(未实测)
- Intellij Idea 社区版
- javassist 3.27.0-GA
javassist 简介
javassist 是什么东西
首先, 放上 官网 , 简而言之, 这东西是一个库, 可以用来修改 java 的字节码
同时, 这东西不需要你了解太多的 class 在储存为.class 文件时的储存方式, 但需要你对于 java 反射有一定的了解, 因为这东西是以 jar 包的方式引入到 java 应用中, 然后可以通过封装的方式来修改 class 内方法实现
包含但不限于如下功能
- 添加,删除字段, 方法, 类. 包
- 修改方法, 类可见性
- 修改方法的实现体
应用范围
那么, 这东西有啥用呢?
比如, 有一个库是上古时期的人提供的, 没有源码, 没有文档, 开发者早联系不上了, 但我们可能需要修改其中的一个实现
你可能会想: 反编译啊, 重打包啊
但事实上很难行得通, 因为你重新编译时可能需要找到它当时依赖的所有 jar 包, 然后循环依赖引入, 或者可能你的 jar 包是一个安卓 jar 包, 所以需要安卓环境, 而把 android.jar 包引入 java 工程, 想想就很带感
而使用 javassist, 这步会显得很简单
示例
-
新建一个项目
-
引入 javassist, 本地引入或 maven, gradle 什么的都随你习惯, 我这里用的是 gradle+本地 jar 包
编写代码
原始代码
1package top.kikt;
2
3public class User {
4
5 void say() {
6 System.out.println("The user say!");
7 }
8
9}
1package top.kikt;
2
3public class HelloWorld {
4
5 public static void main(String[] args) {
6 User user = new User();
7 user.say();
8 }
9}
嗯 就这样, 这个简单的项目就这样, 运行结果也很简单, 就是The user say!
修改 say 的实现
1package top.kikt;
2
3import javassist.*;
4
5import java.io.IOException;
6
7public class Crack {
8
9 public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
10 ClassPool pool = ClassPool.getDefault();
11 CtClass ctClass = pool.get("top.kikt.User");
12 CtMethod[] says = ctClass.getDeclaredMethods("say");
13 CtMethod say = says[0];
14 say.setBody("{System.out.println(\"The user crack say!\");}");
15
16 ctClass.writeFile("build/crack/java/main");
17 }
18
19}
我这里就是简单的修改了 say 的实现, 简单分析
- 获取一个 pool, 这个东西可以说是 assist 的核心类了,
- 根据类名, 获取 User 类的字节码
- 然后找到 say 方法, 因为我们有源码, 所以可以确定序号 0 就是这个方法
- 然后 setBody 就是新实现了
- class.writeFile 这东西很有意思, 会按正规 jar 包的内 class 的储存方式生成 class 文件
实现修改
打包旧实现
1./gradlew jar
这样会在 build/libs 下生成一个 jar 包
我们运行一下:
java -cp build/libs/CrackTest-1.0-SNAPSHOT.jar top.kikt.HelloWorld
我们看到, 实现的 User.java 里的内容
生成 class 文件
接着就是运行这个 Crack.main 方法了
我们通过 idea 自带的 class 文件查看器看到, say 方法的实现果然变了
合并 class 和 jar 包
这一步需要利用 jar 命令, 这个命令是 jdk 自带的
1$ which jar
2/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/jar
3
4$ jar
5用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
6选项:
7 -c 创建新档案
8 -t 列出档案目录
9 -x 从档案中提取指定的 (或所有) 文件
10 -u 更新现有档案
11 -v 在标准输出中生成详细输出
12 -f 指定档案文件名
13 -m 包含指定清单文件中的清单信息
14 -n 创建新档案后执行 Pack200 规范化
15 -e 为捆绑到可执行 jar 文件的独立应用程序
16 指定应用程序入口点
17 -0 仅存储; 不使用任何 ZIP 压缩
18 -P 保留文件名中的前导 '/' (绝对路径) 和 ".." (父目录) 组件
19 -M 不创建条目的清单文件
20 -i 为指定的 jar 文件生成索引信息
21 -C 更改为指定的目录并包含以下文件
22如果任何文件为目录, 则对其进行递归处理。
23清单文件名, 档案文件名和入口点名称的指定顺序
24与 'm', 'f' 和 'e' 标记的指定顺序相同。
25
26示例 1: 将两个类文件归档到一个名为 classes.jar 的档案中:
27 jar cvf classes.jar Foo.class Bar.class
28示例 2: 使用现有的清单文件 'mymanifest' 并
29 将 foo/ 目录中的所有文件归档到 'classes.jar' 中:
30 jar cvfm classes.jar mymanifest -C foo/ .
和 tar 的用法差不多, 总体来说就是
1jar uvf xxx.jar xxx.class xxx.class
大概就是这样, 但我们的 jar 在 build/libs 目录里, 而其他的在 crack 目录里, 所以这里我编写一个简单的脚本来做这个事
1touch merge_class.sh
2chmod +x *.sh
merge_class.sh
1ROOT_PATH=$PWD
2cd build/crack/java/main
3jar uvf $ROOT_PATH/build/libs/CrackTest-1.0-SNAPSHOT.jar . # 不要忘记最后的点
简单解释一下, u是工作模式, 更新现有的jar包, v是日志, f 是指定目录, 工作目录如果不是class目录, 则打包成jar的时候
脚本运行结果:
1$ ./merge_class.sh
2正在添加: top/(输入 = 0) (输出 = 0)(存储了 0%)
3正在添加: top/kikt/(输入 = 0) (输出 = 0)(存储了 0%)
4正在添加: top/kikt/User.class(输入 = 452) (输出 = 311)(压缩了 31%)
1java -cp build/libs/CrackTest-1.0-SNAPSHOT.jar top.kikt.HelloWorld
实现修改成功了!
核心类的简单说明
- ClassPool, 核心类, 一般可以用过
ClassPool.getDefault
获取默认实例 - CtClass, 表示类
- 转 java 的 Class 类:
ctClass.toClass()
- 创建新的:
pool.makeClass
- 获取已有的类,
pool.get(String name)
- 转 java 的 Class 类:
- CtField, 表示字段(成员变量)
- 获取所有的:
ctClass.getFields()
- 根据名字获取:
ctClass.getField(String name)
- 获取所有的:
- CtMethod, 表示方法
ctClass.getMethods()
ctClass.getMethod(String name)
后记
本篇就简单的演示了一下入门级使用, 预计下篇写一些实际项目中的使用
以上