Java反编译不是编程语言而是逆向解析过程,输出近似源码但存在泛型擦除、变量名丢失等不可逆信息损失;字节码操作需直接处理JVM指令或通过ASM/Javassist,须注意栈平衡、签名正确性及运行时类修改限制。

Java 反编译本身没有“基础语法”——它不是一门语言,而是对 .class 文件的逆向解析过程;字节码操作也并非写 Java 源码,而是直接读写 JVM 指令集(如 iload、invokevirtual)或通过工具库(如 ASM、Javassist)生成/修改字节码。混淆概念容易踩坑:把反编译结果当源码用、拿字节码指令硬背、试图用 javac 编译字节码文本,都会失败。
反编译工具输出的是近似源码,不是原始代码
反编译器(如 jd-gui、fernflower、procyon)输入是 .class,输出是 Java 源码风格的文本,但存在不可逆信息丢失:
- 泛型类型擦除后无法还原(
List反编译出来只剩List) - 局部变量名、方法参数名(除非编译时加
-g:vars)通常变成arg0、local1 - lambda 表达式会被转成合成方法(
lambda$main$0),结构失真 - 内联优化、逃逸分析等 JIT 行为不影响字节码,但反编译器看不到这些运行时变化
实际建议:用 javap -c -s -l 看原始字节码和行号表,比依赖图形化反编译器更可靠。
javap -c -s -l MyClass.class
ASM 中的 MethodVisitor 是字节码改写的实际入口
ASM 不解析 Java 语法,只处理指令流。你要修改一个方法,必须继承 MethodVisitor,在 visitInsn、visitVarInsn、visitMethodInsn 等回调中插入/替换/跳过指令。常见错误包括:
立即学习“Java免费学习笔记(深入)”;
- 忽略栈平衡:在
visitInsn(Opcodes.ICONST_1)后没配对 pop,导致 VerifyError - 调用
visitMethodInsn时传错desc(方法签名),比如把(I)Ljava/lang/String;写成(I)Ljava/lang/Object; - 在
visitCode()前就调用visitVarInsn,触发IllegalStateException - 未重写
visitMaxs,让 ASM 自动计算栈帧(推荐设为-1, -1,交由ClassWriter.COMPUTE_FRAMES处理)
示例:在方法开头插入日志打印(简化版)
public class LogInsertAdapter extends MethodVisitor {
public LogInsertAdapter(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
super.visitCode();
// 插入 System.out.println("ENTER")
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("ENTER");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
Javassist 比 ASM 更接近“源码级”操作,但仍有字节码约束
CtMethod.insertBefore() 看似能写 Java 代码,但它背后仍要转成字节码。以下写法会报错:
- 调用未导入的类:
insertBefore("MyUtil.doLog();")→ 必须先ctClass.getClassPool().importPackage("com.example") - 使用局部变量声明:
insertBefore("int x = 1;")→ Javassist 不支持语句块,只能是表达式或单条语句(如赋值、方法调用) - 引用 this 字段但目标类没该字段:
insertBefore("this.id = 0;")→ 运行时报CannotCompileException - 插入含 try-catch 的代码 → Javassist 8+ 才支持,旧版直接抛异常
安全做法:用 $1, $2 引用参数,$_ 引用返回值,避免硬编码变量名。
javac 编译的 class 和运行时加载的 class 不是一回事
你用 javac 编译出的 .class 是静态字节码,但 JVM 加载时可能被 agent 修改(如 Spring Loaded、JRebel)、被 Instrumentation 重定义、甚至被类加载器动态生成(如 CGLIB)。这意味着:
- 反编译线上出问题的 class,可能和你本地编译的版本不一致(尤其用了热部署或 AOP)
- 用
ClassLoader.getResourceAsStream("X.class")读到的字节码,未必是磁盘上的原始文件(可能是被 transform 后的) - 想确认真实字节码,要用
Instrumentation.getAllLoadedClasses()+Instrumentation.getBytecode()(需 premain agent)
最易被忽略的一点:JDK 9+ 的模块系统(java.base 等)默认禁止反射修改内部类,连 Unsafe.defineClass 都可能被 SecurityManager 拦截——不是代码写错了,是权限模型变了。










