字节码增强技术
JVM提供的动态代理和spring AOP就是字节码增强技术。两种实现机制:一种是通过创建原始类的一个子类,现在的SpringAOP正式通过这种方式实现,另一种是非常暴力的,即直接修改原先的Class字节码
字节码增强步骤:
- 在内存中获取到原始的字节码,然后通用一些开源提供的API(ASM,javassist,cglib等技术)来修改它的byte[]数组,得到一个新的byte[];
- 将这个新的数组写到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"); } } |