本篇仅用于记录本人学习javassist的过程, 其中任何步骤或思想被用于非法用途与本人无关

环境介绍

  • macOS
  • Jdk8, 因为我是安卓开发, 事实上理论上也支持 14(未实测)
  • Intellij Idea 社区版
  • javassist 3.27.0-GA

javassist 简介

javassist 是什么东西

首先, 放上官网, 简而言之, 这东西是一个库, 可以用来修改 java 的字节码

同时, 这东西不需要你了解太多的 class 在储存为.class 文件时的储存方式, 但需要你对于 java 反射有一定的了解, 因为这东西是以 jar 包的方式引入到 java 应用中, 然后可以通过封装的方式来修改 class 内方法实现

包含但不限于如下功能

  1. 添加,删除字段, 方法, 类. 包
  2. 修改方法, 类可见性
  3. 修改方法的实现体

应用范围

那么, 这东西有啥用呢?

比如, 有一个库是上古时期的人提供的, 没有源码, 没有文档, 开发者早联系不上了, 但我们可能需要修改其中的一个实现

你可能会想: 反编译啊, 重打包啊

但事实上很难行得通, 因为你重新编译时可能需要找到它当时依赖的所有 jar 包, 然后循环依赖引入, 或者可能你的 jar 包是一个安卓 jar 包, 所以需要安卓环境, 而把 android.jar 包引入 java 工程, 想想就很带感

而使用 javassist, 这步会显得很简单

示例

  1. 新建一个项目

  2. 引入 javassist, 本地引入或 maven, gradle 什么的都随你习惯, 我这里用的是 gradle+本地 jar 包

编写代码

原始代码

package top.kikt;

public class User {

    void say() {
        System.out.println("The user say!");
    }

}
package top.kikt;

public class HelloWorld {

    public static void main(String[] args) {
        User user = new User();
        user.say();
    }
}

嗯 就这样, 这个简单的项目就这样, 运行结果也很简单, 就是The user say!

修改 say 的实现

package top.kikt;

import javassist.*;

import java.io.IOException;

public class Crack {

    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("top.kikt.User");
        CtMethod[] says = ctClass.getDeclaredMethods("say");
        CtMethod say = says[0];
        say.setBody("{System.out.println(\"The user crack say!\");}");

        ctClass.writeFile("build/crack/java/main");
    }

}

我这里就是简单的修改了 say 的实现, 简单分析

  1. 获取一个 pool, 这个东西可以说是 assist 的核心类了,
  2. 根据类名, 获取 User 类的字节码
  3. 然后找到 say 方法, 因为我们有源码, 所以可以确定序号 0 就是这个方法
  4. 然后 setBody 就是新实现了
  5. class.writeFile 这东西很有意思, 会按正规 jar 包的内 class 的储存方式生成 class 文件

实现修改

打包旧实现

./gradlew jar

这样会在 build/libs 下生成一个 jar 包

我们运行一下:

java -cp build/libs/CrackTest-1.0-SNAPSHOT.jar top.kikt.HelloWorld

image-20201003205347178

我们看到, 实现的 User.java 里的内容

生成 class 文件

接着就是运行这个 Crack.main 方法了

image-20201003205537490

我们通过 idea 自带的 class 文件查看器看到, say 方法的实现果然变了

合并 class 和 jar 包

这一步需要利用 jar 命令, 这个命令是 jdk 自带的

$ which jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/jar

$ jar
用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
选项:
    -c  创建新档案
    -t  列出档案目录
    -x  从档案中提取指定的 (或所有) 文件
    -u  更新现有档案
    -v  在标准输出中生成详细输出
    -f  指定档案文件名
    -m  包含指定清单文件中的清单信息
    -n  创建新档案后执行 Pack200 规范化
    -e  为捆绑到可执行 jar 文件的独立应用程序
        指定应用程序入口点
    -0  仅存储; 不使用任何 ZIP 压缩
    -P  保留文件名中的前导 '/' (绝对路径) 和 ".." (父目录) 组件
    -M  不创建条目的清单文件
    -i  为指定的 jar 文件生成索引信息
    -C  更改为指定的目录并包含以下文件
如果任何文件为目录, 则对其进行递归处理。
清单文件名, 档案文件名和入口点名称的指定顺序
与 'm', 'f' 和 'e' 标记的指定顺序相同。

示例 1: 将两个类文件归档到一个名为 classes.jar 的档案中:
       jar cvf classes.jar Foo.class Bar.class
示例 2: 使用现有的清单文件 'mymanifest' 并
           将 foo/ 目录中的所有文件归档到 'classes.jar' 中:
       jar cvfm classes.jar mymanifest -C foo/ .

和 tar 的用法差不多, 总体来说就是

jar uvf xxx.jar xxx.class xxx.class

大概就是这样, 但我们的 jar 在 build/libs 目录里, 而其他的在 crack 目录里, 所以这里我编写一个简单的脚本来做这个事

touch merge_class.sh
chmod +x *.sh

merge_class.sh

ROOT_PATH=$PWD
cd build/crack/java/main
jar uvf $ROOT_PATH/build/libs/CrackTest-1.0-SNAPSHOT.jar . # 不要忘记最后的点

简单解释一下, u是工作模式, 更新现有的jar包, v是日志, f 是指定目录, 工作目录如果不是class目录, 则打包成jar的时候

脚本运行结果:

$ ./merge_class.sh
正在添加: top/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: top/kikt/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: top/kikt/User.class(输入 = 452) (输出 = 311)(压缩了 31%)
java -cp build/libs/CrackTest-1.0-SNAPSHOT.jar top.kikt.HelloWorld

image-20201003210352423

实现修改成功了!

核心类的简单说明

  • ClassPool, 核心类, 一般可以用过ClassPool.getDefault获取默认实例
  • CtClass, 表示类
    • 转 java 的 Class 类: ctClass.toClass()
    • 创建新的: pool.makeClass
    • 获取已有的类, pool.get(String name)
  • CtField, 表示字段(成员变量)
    • 获取所有的: ctClass.getFields()
    • 根据名字获取: ctClass.getField(String name)
  • CtMethod, 表示方法
    • ctClass.getMethods()
    • ctClass.getMethod(String name)

后记

本篇就简单的演示了一下入门级使用, 预计下篇写一些实际项目中的使用

仓库

以上