这个类,就是官方jdk提供的类,官方的本意呢,肯定是让大家,在加载class的时候,给大家提供一个机会,去修改class,比如,某个第三方jar包,我们需要修改,但是没有源码,就可以这么干;或者是一些要统一处理,不方便在应用中耦合的功能:比如埋点、性能监控、日志记录、安全监测等。
说回这个方法,参数为ClassFileTransformer,这个接口,就一个方法,大家看看注释:
/** * ... * * @param classfileBuffer the input byte buffer in class file format - must not be modified * * @throws IllegalClassFormatException if the input does not represent a well-formed class file * @return a well-formed class file buffer (the result of the transform), or <code>null</code> if no transform is performed. * @see Instrumentation#redefineClasses */ byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;classfileBuffer,就是原始class字节码数组,官方注释说:一定不能修改它
返回的byte[]数组,注释:一个格式正确的class文件数组,或者null,表示没有进行转换
别的也不多说了,反正就是:jvm给你原始class,你自己修改,还jvm一个改后的class。
所以,大家估计也能猜到破解的原理了,但我还是希望大家:有能力支持正版的话,还是要支持。
接下来,我们回到我们的目标的实现上。
agent模块开发完整代码:https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/java-agent-premain-demo
增加类转换器 package org.xunche.agent; import org.objectweb.asm.*; import org.objectweb.asm.commons.AdviceAdapter; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; public class TimeAgentByJava { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new TimeClassFileTransformer()); } }类转换器的详细代码如下:
private static class TimeClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun") || className.startsWith("com/sun")|| className.startsWith("org/xunche/agent")) { //return null或者执行异常会执行原来的字节码 return null; } // 1 System.out.println("loaded class: " + className); ClassReader reader = new ClassReader(classfileBuffer); // 2 ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); // 3 reader.accept(new TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES); // 4 return writer.toByteArray(); } }
1处,将原始的类字节码加载到classReader中
ClassReader reader = new ClassReader(classfileBuffer);
2处,将reader传给ClassWriter,这个我们没讲过,大概就是使用classreader中的东西,来构造ClassWriter;可以差不多理解为复制classreader的东西到ClassWriter中。
大家可以看如下代码:
public ClassWriter(final ClassReader classReader, final int flags) { super(Opcodes.ASM6); symbolTable = new SymbolTable(this, classReader); ... }这里new了一个对象,SymbolTable。
SymbolTable(final ClassWriter classWriter, final ClassReader classReader) { this.classWriter = classWriter; this.sourceClassReader = classReader; // Copy the constant pool binary content. byte[] inputBytes = classReader.b; int constantPoolOffset = classReader.getItem(1) - 1; int constantPoolLength = classReader.header - constantPoolOffset; constantPoolCount = classReader.getItemCount(); constantPool = new ByteVector(constantPoolLength); constantPool.putByteArray(inputBytes, constantPoolOffset, constantPoolLength); ... }大家直接看上面的注释吧,Copy the constant pool binary content。反正吧,基本可以理解为,classwriter拷贝了classreader中的一部分东西,应该不是全部。
为什么不是全部,因为我试了下:
public static void main(String[] args) throws IOException { ClassReader reader = new ClassReader("org.xunche.app.HelloXunChe"); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); byte[] bytes = writer.toByteArray(); File file = new File( "F:\\gitee-ckl\\all-simple-demo-in-work\\java-agent-premain-demo\\test-agent\\src\\main\\java\\org\\xunche\\app\\HelloXunChe.class"); FileOutputStream fos = new FileOutputStream(file); fos.write(bytes); fos.close(); }上面这样,出来的class文件,是破损的,格式不正确的,无法反编译。
3处,使用TimeClassVisitor作为writer的中间商,此时,顺序变成了:
classreader --> TimeClassVisitor --> classWriter