字节码增强技术

       JVM提供的动态代理和spring AOP就是字节码增强技术。两种实现机制:一种是通过创建原始类的一个子类,现在的SpringAOP正式通过这种方式实现,另一种是非常暴力的,即直接修改原先的Class字节码

java字节码编程技术(4/10)-字节码增强技术_JVM

字节码增强步骤:

  1. 在内存中获取到原始的字节码,然后通用一些开源提供的API(ASM,javassist,cglib等技术)来修改它的byte[]数组,得到一个新的byte[];
  2. 将这个新的数组写到PermGen区域,也就是加载它或替换原来的Class字节码(也可以在进程外部调用完成)

反射和ASM区别

  • 反射是读取持久堆上存储的类信息。而 ASM 是直接处理 .class 字节码的工具。
  • 反射只能读取类信息,而 ASM 除了读还能写。
  • 反射读取类信息时需要进行类加载处理,而 ASM 则不需要将类加载到内存中。
  • 反射相对于 ASM 来说使用方便,想直接操纵 ASM 的话需要有 JVM 指令基础。

java.lang.instrument

java.lang.instrument包下的内容,它是官方开放的字节码工具。提供了注册类文件转换器、获取所有已加载的类等功能,基于它可以实现热部署、JVM级别AOP等高级功能;开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序。重定义可能会更改方法体、常量池和属性。但不得添加、移除、重命名字段或方法。

之所以能实现热部署是因为有些方法可以触发让JVM重新加载已加载的类。

Agent-Prmain静态代理

这种方法是处用prmain和main中间的空档期内,通过ASM等技术修改字节码;这种方法比较局限,它只能在JVM启动时指定代理。

要修改的目标类

/**在此类做加载时,进行处理*/

public class TransClass {

public void sayHello() {

System.out.println("Hello world");

}

}

自定义的classFileTransformer

public class Transformer implements ClassFileTransformer {


/**

* addTransformer 方法并没有指明要转换哪个类。

* 转换发生在 premain 函数执行之后,main 函数执行之前,每装载一个类transform 方法就会执行一次,所以此处需要判断具体要操作哪些类,否则所有的类都会进行转换

* 此处需要注意className的形式。*/

@Override

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,

ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

System.out.println("transform()");

if(className.equals("com/instrumentation/TransClass")) {

ClassPool classPool = ClassPool.getDefault();


try {

//使用 javassist 技术对字节码进行处理,对TransClass类的sayHello方法体前插入一行代码

CtClass class1 = classPool.get(className.replaceAll("/", "."));

CtMethod ctMethod = class1.getDeclaredMethod("sayHello");

if(!ctMethod.isEmpty()) {

ctMethod.insertBefore("System.out.println(\"before hello!!!\");");

}

return class1.toBytecode();

} catch (NotFoundException | CannotCompileException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

return null;

}

}

定义prmain

public class AgentMain {

/**

* inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。

* */

public static void premain(String agentArgs,Instrumentation inst) throws ClassNotFoundException,UnmodifiableClassException {

inst.addTransformer(new Transformer());

System.out.println("premain ok!");

}

}

测试类

public class TestMainInJar {

public static void main(String[] args) {

System.out.println("TestMainInJar main()");

new TransClass().sayHello();

}

}

打包配置

Manifest-Version: 1.0

Premain-Class: com.instrumentation.AgentMain

Main-Class: com.instrumentation.TestMainInJar

运行

java -javaagent:jvmclazz-1.0-SNAPSHOT.jar TestMainInJar


Agent-Attach动态启动

这种方式可以JVM启动后动态代理,除了以下两组代码,其它代码全一样。这使用了同一主机进程间通信技术,基于unix套接字技术来实现。

设置代理

public class AttackAgentMain {

/**

* inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。

* */

public static void agentmain(String agentArgs,Instrumentation inst) throws ClassNotFoundException,UnmodifiableClassException {

inst.addTransformer(new Transformer());

System.out.println("premain ok!");

}

}

启动

//执行此类,传递要被代理类的PID

public class AttackTestMain {


public static void main(String []args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {

VirtualMachine vm = VirtualMachine.attach(args[0]);

vm.loadAgent("agent.jar");

}

}