ASM是一款基于java字节码层面的代码分析和修改工具;无需提供源代码即可对应用嵌入所需debug代码,用于应用API性能分析,代码优化和代码混淆等工作。ASM的目标是生成,转换和分析已编译的java class文件,可使用ASM工具读/写/转换JVM指令集。         ASM工具提供两种方式来产生和转换已编译的class文件,它们分别是基于事件和基于对象的表示模型。其中,基于事件的表示模型使用一个有序的事件序列表示一个class文件,class文件中的每一个元素使用一个事件来表示,比如class的头部,变量,方法声明JVM指令都有相对应的事件表示,ASM使用自带的事件解析器能将每一个class文件解析成一个事件序列。而基于对象的表示模型则使用对象树结构来解析每一个文件。


基于事件模型的ASM工具使用生产者-消费者模型转换/产生一个class文件。其转换过程中涉及到自定义的事件生产者,自定义的事件过滤器和自定义的事件消费者这三种组件。其中使用classReader来解析每一个class文件中的事件元素,使用自定义的各种基于方法/变量/声明/类注释的元素适配器来过滤和修改class事件元序列中的相应事件对象,最后使用ClassWriter来重新将更新后的class事件序列转换成class字节码供JVM加载执行。整个生产/转换class文件的过程如下图所示,起点和终点分别是CLASSREADER(CLASS文件解析器)和ClassWriter(class事件序列转换到class字节码),中间的过程由若干个自定义的事件过滤器组成。
1.class文件的结构
class文件保持固定的结构信息,而且保留了几乎所有的源代码文件中的符号。一个class文件整体结构由几个区域组成,一个区域用来描述类的modifier,name,父类,接口和注释。一个区域用来描述类中变量的modfier,名字,类型和注释。一个区域用来描述类中方法和构造函数的modifier,名字参数类型,返回类型,注释等信息,当然也包含已编译成java字节码指令序列的方法具体内容。还有一个作为class文件的静态池区域,用来保存所有的数字,字符串,类型的常量,这些常量只被定义过一次且被其他class中区域所引用。class文件与源代码文件的关系:一个java文件最后会被编译成N(1 <= N)个class文件。
下图展示了一个class文件的总体概貌


图 2.1 class文件的概览(*表示0个或者多个)

2. class文件的内部命名
原java类型与class文件内部类型对应关系

图2.2 java类型的描述

原java方法声明与class文件内部方法声明的对应关系

图2.3方法描述举例

3.ASM工具的接口和组件
ASM工具生产和转换class文件内容的所有工作都是基于ClassVisitor这个抽象类进行的。ClassVisitor抽象类中的每一个方法会对应到class文件的相应区域,每个方法负责处理class文件相应区域的字节码内容。下图展示了ClassVisitor抽象类的成员函数。

public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc); AnnotationVisitor visitAnnotation(String desc, boolean visible); public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName, String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc,String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions); void visitEnd();
}

某些区域方法直接完成该区域字节码内容的处理并返回空,某些复杂class区域的方法需返回更加细节的XXXVisitor对象,此XXXVisitor对象将负责处理此class区域的更加细节的字节码内容,开发者可以编写继承自XXXVisitor抽象类的自定义类,在成员函数中实现对细节字节码操作的逻辑代码。比如,visitField方法用来负责class文件中变量区域的字节码内容修改,该区域又可细分出多种属性数据对象(注释,参数值),这里需要编写继承自fieldVisitor抽象类的自定义类完成这些细分数据对象的字节码内容操作。下图为FieldVisitor的抽象类内容。

public abstract class FieldVisitor {
public FieldVisitor(int api);
public FieldVisitor(int api, FieldVisitor fv);
public AnnotationVisitor visitAnnotation(String desc, boolean visible); public void visitAttribute(Attribute attr);
public void visitEnd();
}

另外,在处理整个class文件处理过程中,classVistor抽象类中的方法访问需满足下面的次序。

visitvisitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*

(visitInnerClass | visitField | visitMethod )*
visitEnd

基于 ClassVistor api 的访问方式, ASM 工具提供了三种核心组件用来实现 class 的产生和转换工作。 ClassReader 负责解析 class 文件字节码数组,然后将相应区域的内容对象传递给 classVistor 实例中相应的 visitXXX 方法, ClassReader 可以看作是一个事件生产者。 ClassWriter 继承自 ClassVistor 抽象类,负责将对象化的 class 文件内容重构成一个二进制格式的 class 字节码文件, ClassWriter 可以看作是一个事件消费者。继承自 ClassVistor 抽象类的自定义类负责 class 文件各个区域内容的修改和生成,它可以看作是一个事件过滤器,一次生产消费过程中这样的事件过滤器可以有 N 个( 0<=N )。

1、遍历CLASS字节码类信息
面的例子用来打印class字节码内容的类信息,这里以java.lang.runnable为例。
test.java:

public class ClassPrinter extends ClassVisitor { public ClassPrinter() {super(ASM4);}public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {System.out.println(name + " extends " + superName + " {");}public void visitSource(String source, String debug) {}public void visitOuterClass(String owner, String name, String desc) {}public AnnotationVisitor visitAnnotation(String desc, boolean visible) {return null;}public void visitAttribute(Attribute attr) {}public void visitInnerClass(String name, String outerName, String innerName, int access) {}public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {System.out.println("    " + desc + " " + name);return null;}public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {System.out.println(" " + name + desc);return null;}public void visitEnd() {System.out.println("}");}
}//ClassReader作为字节码生产者,ClassPrinter作为字节码消费者
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);

输出:

java/lang/Runnable extends java/lang/Object {run()V
}

2 、生产自定义类对应的 class 字节码内容
目标生产出以下自定义接口:

package pkg;
public interface Comparable extends Mesurable {int LESS = -1;int EQUAL = 0;int GREATER = 1;int compareTo(Object o);
}

test.java内容:

ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "pkg/Comparable", null, "java/lang/Object", new String[] { "pkg/Mesurable" });
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I", null, new Integer(1)).visitEnd(); cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null).visitEnd(); cw.visitEnd();
byte[] b = cw.toByteArray();

3、动态加载2生产出的class字节码并实例化该类
使用继承自ClassLoader的类,并重写defineClass方法;
test.java:

//第一种方法:通过ClassLoader的defineClass动态加载字节码
class MyClassLoader extends ClassLoader {public Class defineClass(String name, byte[] b) {return defineClass(name, b, 0, b.length);}
}
//直接调用方法
Class c = myClassLoader.defineClass("pkg.Comparable", b);

使用继承自ClassLoader的类,并重写findClass内部类;
test.java:

class StubClassLoader extends ClassLoader {@Overrideprotected Class findClass(String name) throws ClassNotFoundException {if (name.endsWith("_Stub")) {ClassWriter cw = new ClassWriter(0);
...
byte[] b = cw.toByteArray();return defineClass(name, b, 0, b.length);}return super.findClass(name);}
}

4、转换class字节码内容中类的成员(变量,方法,注释等)

4.1、ClassReader生产者生产的class字节码bytes可以被ClassWriter直接消费,比如:

byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); //这里的b2与b1表示同一个类且值一样

4.2、ClassReader生产者生产的class字节码bytes可以先被继承自ClassVisitor的自定义类过滤,最后被ClassWriter消费,比如:

byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ChangeVersionAdapter (cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); //这里的b2与b1表示同一个类但值不一样

具体架构图如下如所示,

图2.6类字节码转换链(bytes->reader->adapter->writer->bytes)

这里的ChangeVersionAdapter继承自ClassVisitor,它没做其他过滤,仅仅重写了visit方法,过滤出类中的方法并指定方法的版本号(v1.5)。

public class ChangeVersionAdapter extends ClassVisitor { public ChangeVersionAdapter(ClassVisitor cv) {super(ASM4, cv);}@Overridepublic void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { cv.visit(V1_5, access, name, signature, superName, interfaces);}
}

整个字节码转换过程的时序图如下所示。

图2.7类字节码使用ChangeVersionAdapter过滤转换过程的时序图

通过重写ClassVisitor的visit方法并修改cv.visit方法中的其他参数,你还能做其他有趣的事情。比如,你能给这个class增加一个接口,改变这个class的名字等。

4.3、如何使用转换过的class类字节码

前面的小节提到,转换后的classb2能够保存到磁盘中或者通过ClassLoader动态加载。通过这种动态加载的方式只能在本地使用这个ClassLoader加载的单个类,无法满足同时加载多个类的需求。如果你想要转换所有运行时的类字节码并能够加载使用这些转换后的类字节码文件(被systemClassLoader加载),你需要使用java.lang.instrument包中的ClassFileTransformer类。实现继承自ClassFileTransformer的自定义类并重写transform方法,在该方法中实现对所有系统class文件的遍历(这里的遍历指对各个class文件在字节码层面的转换)。另外这里的ClassFileTransformer必须在main方法之前完成class文件的转换,且必须与main方法在同一个JVM中运行,这样才能被system classLoader加载。

那么怎么实现字节码转换发生在main方法执行之前?这里涉及到java agent的概念,java代理(agent) 作为main方法前的一个拦截器 (interceptor),也就是在main方法执行之前,执行agent的代码。agent的代码与你的main方法在同一个JVM中运行,并被同一个system classloader装载,被同一的安全策略 (security policy) 和上下文 (context) 所管理。

怎样写一个java agent? 只需要实现premain这个方法

public static void premain(String agentArgs,Instrumentation inst)

下面的例子展示了如何实现对所有运行时class字节码进行转换并加载的方法。

public static void premain(String agentArgs, Instrumentation inst) {//该方法在main方法前运行inst.addTransformer(new ClassFileTransformer() {public byte[] transform(ClassLoader l, String name, Class c, ProtectionDomain d, byte[] b) throws IllegalClassFormatException {ClassReader cr = new ClassReader(b);ClassWriter cw = new ClassWriter(cr, 0);ClassVisitor cv = new ChangeVersionAdapter(cw);cr.accept(cv, 0);return cw.toByteArray();} });
}

5、删除class中的方法或者变量的做法

通过在visitMethod或者visitField方法中返回null的方式就能实现删除class中的方法或者变量的需求。如下实例,通过传入名字和描述删除class中的相应方法。

public class RemoveMethodAdapter extends ClassVisitor {private String mName;private String mDesc;public RemoveMethodAdapter(ClassVisitor cv, String mName, String mDesc) {super(ASM4, cv);this.mName = mName;this.mDesc = mDesc;}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {if (name.equals(mName) && desc.equals(mDesc)) {// do not delegate to next visitor -> this removes the methodreturn null;}return cv.visitMethod(access, name, desc, signature, exceptions);}
}

6、增加class中的方法或者变量的做法

在visitEnd中增加想要增加的方法或者变量,实例如下。

public class AddFieldAdapter extends ClassVisitor {private int fAcc;private String fName;private String fDesc;private boolean isFieldPresent;public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) {super(ASM4, cv);this.fAcc = fAcc;this.fName = fName;this.fDesc = fDesc;}@Overridepublic FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {if (name.equals(fName)) {isFieldPresent = true;}return cv.visitField(access, name, desc, signature, value);}@Overridepublic void visitEnd() {if (!isFieldPresent) {FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);if (fv != null) {fv.visitEnd();}}cv.visitEnd();}
}

7、classVisitor过滤(转换)链

可在一次 生产—过滤–消费 过程中,使用多个继承自classVisitor的自定义过滤类对class进行过滤(转换)。

图2.8过滤链

下面的实例中通过传入多个自定义的过滤器,实现过滤链的功能。

public class MultiClassAdapter extends ClassVisitor { protected ClassVisitor[] cvs;public MultiClassAdapter(ClassVisitor[] cvs) {super(ASM4);this.cvs = cvs;}@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {for (ClassVisitor cv : cvs) {cv.visit(version, access, name, signature, superName, interfaces);}}...
}

8、最后通过一个例子来梳理一下asm工具的使用方法

例子中用到了agentmain,先补充一下相关知识。

agentmain方式

premain时JavaSE5开始就提供的代理方式,给了开发者诸多惊喜,不过也有些须不变,由于其必须在命令行指定代理jar,并且代理类必须在main方法前启动。因此,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较苦难的。比如正常的生产环境下,一般不会开启代理功能,但是在发生问题时,我们不希望停止应用就能够动态的去修改一些类的行为,以帮助排查问题,这在应用启动前是无法确定的。 为解决运行时启动代理类的问题,JavaSE6开始,提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 与permain类似,agent方式同样需要提供一个agentjar,并且这个jar需要满足:

1.    在manifest中指定Agent-Class属性,值为代理类全路径

2.    代理类需要提供public static voidagentmain(String args, Instrumentation inst)或public static void agentmain(Stringargs)方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致。

不过如此设计的再运行时进行代理有个问题——如何在应用程序启动之后再开启代理程序呢?JDK6中提供了JavaTools API,其中AttachAPI可以满足这个需求。

Attach API中的VirtualMachine代表一个运行中的VM。其提供了loadAgent()方法,可以在运行时动态加载一个代理jar。具体需要参考《Attach API》(http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html

转自:http://blog.sina.com.cn/s/blog_605f5b4f01010i3b.html

完整的例子如下,在java.lang.ProcessBuilder实例调用start方法时eclipse的console中打印出"java.lang.ProcessBuilder实例的start方法被触发"。

代理类RewriterAgent.java:

public class RewriterAgent {public static void agentmain(String agentArgs, Instrumentation instrumentation){ //代理类入口函数premain(agentArgs, instrumentation);}public static void premain(String agentArgs, Instrumentation instrumentation){ //代理类入口函数Java SE6try{DexClassTransformer dexClassTransformer = new DexClassTransformer(); //继承自ClassFileTransformer的自定义类,多class文件的加载instrumentation.addTransformer(dexClassTransformer, true); }catch(Throwable ex){System.out.println("Agent startup error");throw new RuntimeException(ex);}}private static final class DexClassTransformer implements ClassFileTransformer{private Log log;private final Map classVisitors;public byte[] transform(ClassLoader classLoader, String className, Class clazz, ProtectionDomain protectionDomain, byte bytes[]) throws IllegalClassFormatException{ClassVisitorFactory factory = (ClassVisitorFactory)classVisitors.get(className); //根据给定className匹配相应class的工厂对象if(factory != null){if(clazz != null && !factory.isRetransformOkay()){return null;}try{ClassReader cr = new ClassReader(bytes);  //生产者ClassWriter cw = new PatchedClassWriter(3, classLoader); //消费者 自定义的ClassWriterClassAdapter adapter = factory.create(cw); //过滤器cr.accept(adapter, 4);return cw.toByteArray();}catch(SkipException ex) { }catch(Exception ex){throw ex;}}return null;}public DexClassTransformer() throws URISyntaxException{classVisitors = new HashMap<String, ClassVisitorFactory>(){  //将需要转换的类放到map中,Map中为不同的类指定相应的工厂对象private static final long serialVersionUID = 1L;{put("java/lang/ProcessBuilder", new ClassVisitorFactory(true){ //实现ClassVisitorFactory并重写create方法public ClassAdapter create(ClassVisitor cv){return RewriterAgent.createProcessBuilderClassAdapter(cv);}});}};}}   private static ClassAdapter createProcessBuilderClassAdapter(ClassVisitor cw, Log log){return new ClassAdapter(cw){public MethodVisitor visitMethod(int access, String name, String desc, String signature, String exceptions[]){MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);if("start".equals(name)){System.out.println("java.lang.ProcessBuilder实例的start方法被触发");}return mv;}};}
}

自定义的ClassWriter:

class PatchedClassWriter extends ClassWriter{private final ClassLoader classLoader;public PatchedClassWriter(int flags, ClassLoader classLoader){super(flags);this.classLoader = classLoader;}protected String getCommonSuperClass(String type1, String type2){ //返回共同的父类Class c;Class d;try{c = Class.forName(type1.replace('/', '.'), true, classLoader);d = Class.forName(type2.replace('/', '.'), true, classLoader);}catch(Exception e){throw new RuntimeException(e.toString());}if(c.isAssignableFrom(d))return type1;if(d.isAssignableFrom(c))return type2;if(c.isInterface() || d.isInterface())return "java/lang/Object";doc = c.getSuperclass();while(!c.isAssignableFrom(d));return c.getName().replace('.', '/');}
}

测试类AttachTest:

public class AttachTest {public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {VirtualMachine vm = VirtualMachine.attach(args[0]);//args[0]传入的是jvm的pid号vm.loadAgent("F:\\workspace\\rewriterAgent.jar"); //rewriterAgent.jar是RewriterAgent.java导出的Jar包名}
}

JAVA字节码增强技术之ASM相关推荐

  1. idea如何反编译字节码指令_美团点评:Java字节码增强技术,线上问题诊断利器...

    作者简介:泽恩,美团到店住宿业务研发团队工程师.文章转载于公众号:美团技术团队 1. 字节码 1.1 什么是字节码? Java之所以可以"一次编译,到处运行",一是因为JVM针对各 ...

  2. Java字节码增强技术

    文章目录 字节码 字节码增强 字节码增强技术 字节码 字节码含义:待补充. Java为了能让Java程序编译一次到处运行,用Java 编译器将程序对源代码编译生成固定格式的字节码(.class文件)供 ...

  3. java 字节码增强原理_深入浅出Java探针技术1--基于java agent的字节码增强案例

    Java agent又叫做Java 探针,本文将从以下四个问题出发来深入浅出了解下Java agent 一.什么是java agent? Java agent是在JDK1.5引入的,是一种可以动态修改 ...

  4. ASM字节码编程 | 用字节码增强技术给所有方法加上TryCatch捕获异常并输出

    作者:小傅哥 博客:https://bugstack.cn Wiki:https://github.com/fuzhengwei/CodeGuide/wiki 沉淀.分享.成长,让自己和他人都能有所收 ...

  5. 干货!Java字节码增强探秘

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"加群"获取公众号专属群聊入口 来源:美团技术团队 1. 字节码 1.1 什么是字节码? ...

  6. JVM插桩之一:JVM字节码增强技术介绍及入门示例

    字节码增强技术:AOP技术其实就是字节码增强技术,JVM提供的动态代理追根究底也是字节码增强技术. 目的:在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修 ...

  7. 字节码增强技术 Byte Buddy 、Javassist、Instrumentation

    概述 字节码增强技术 有 Byte Buddy .Javassist等多种. 如果是在同一个包中,没有问题,不需借助Instrumentation 如果是第三方包,想不修改代码的情况下实现代理技术,就 ...

  8. 探针技术-JavaAgent 和字节码增强技术-Byte Buddy

    能够基于Java Agent编写出普通类的代理 理解Byte Buddy的作用 能够基于Byte Buddy编写动态代理 1 Byte Buddy Byte Buddy 是一个代码生成和操作库,用于在 ...

  9. javassist字节码增强技术

    一.前言: 简单理解就是在原来方法的前后添加新的代码(扩展之前方法的代码) 这个是springAOP切面的基础 (springAOP切面就是给原有代码前后增加新代码)(为了更方便扩充新功能<只编 ...

最新文章

  1. WPF 把图片分割成两份自动翻页 WpfFlipPageControl:CtrlBook 书控件
  2. 写给初中级前端工程师的进阶指南
  3. IIS6.0的web园--最大工作进程数
  4. 从零起步到Linux运维经理,你必须管好的23个细节
  5. 帝国理工:如何用 AI 解决 80% 专科医生担忧的心律装置移植手术难题
  6. java 类库_Java基础类库
  7. matlab进行ai研究,matlab【人工智能项目】—ELM地质分类研究
  8. 19【推荐系统17】MMoE: 多任务学习
  9. 多线程处理同一批数据_Java 多线程基础(一)
  10. HTTP协议 (五) 代理
  11. 第三章 最速下降法和牛顿法
  12. 破解Charles抓包工具
  13. 求素数平均值c语言,C 输入10个正整数到a数组中,求a数组中素数的平均值.
  14. python pygame字体设置_2015/11/3用Python写游戏,pygame入门(3):字体模块、事件显示和错误处理...
  15. 软著申请技巧及注意事项
  16. 基于mysql 批量插入100w测试数据
  17. python爬取豆瓣读书_用python+selenium抓取豆瓣读书中最受关注图书并按照评分排序...
  18. Android之获取外部存储空间解释
  19. Android:Gradle 依赖相关
  20. cs反恐特警3d版2java_CS反恐特警增值版

热门文章

  1. vue-infinite-scroll
  2. 小红书最新入驻,小红书入驻细节,小红书商家入驻
  3. 佳能PIXMAnbsp;MP150/MP170/MP450打印…
  4. CString时间字符串,COleDateTime,CTime类之间的转换
  5. z3 strategies
  6. 星光不问赶路人,时光不负有心人
  7. SSM+在线学习系统 毕业设计-附源码131843
  8. 简述u盘安装计算机系统的方法,计算机装系统需要U盘?最简单的方法
  9. Xcode下载低版本iOS系统模拟器进行调试
  10. pthread_setspecific函数与pthread_getspecific函数