我是靠谱客的博主 落寞胡萝卜,最近开发中收集的这篇文章主要介绍转 : java asm库的原理与使用方法(一),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

原博客地址:

https://blog.csdn.net/xysmiracle/article/details/38293795




在之前android的性能监测sdk项目中用到过asm库,在这里记录一下基本原理和用法;

ASM库/工具  http://asm.ow2.org/

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


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

  1. public abstract class ClassVisitor {

  2. public ClassVisitor(int api);

  3. public ClassVisitor(int api, ClassVisitor cv);

  4. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);

  5. public void visitSource(String source, String debug);

  6. public void visitOuterClass(String owner, String name, String desc);
  7. AnnotationVisitor visitAnnotation(String desc, boolean visible);
  8. public void visitAttribute(Attribute attr);

  9. public void visitInnerClass(String name, String outerName, String innerName, int access);

  10. public FieldVisitor visitField(int access, String name, String desc,
  11. String signature, Object value);

  12. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
  13. void visitEnd();
  14. }

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

  1. public abstract class FieldVisitor {

  2. public FieldVisitor(int api);

  3. public FieldVisitor(int api, FieldVisitor fv);

  4. public AnnotationVisitor visitAnnotation(String desc, boolean visible);
  5. public void visitAttribute(Attribute attr);

  6. public void visitEnd();
  7. }

另外,在处理整个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

  1. public class ClassPrinter extends ClassVisitor {
  2. public ClassPrinter() {
  3. super(ASM4);
  4. }
  5. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  6. System.out.println(name + " extends " + superName + " {");
  7. }
  8. public void visitSource(String source, String debug) {}
  9. public void visitOuterClass(String owner, String name, String desc) {}
  10. public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
  11. return null;
  12. }
  13. public void visitAttribute(Attribute attr) {}
  14. public void visitInnerClass(String name, String outerName, String innerName, int access) {}
  15. public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
  16. System.out.println( " " + desc + " " + name);
  17. return null;
  18. }
  19. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
  20. System.out.println( " " + name + desc);
  21. return null;
  22. }
  23. public void visitEnd() {
  24. System.out.println( "}");
  25. }
  26. }
  27. //ClassReader作为字节码生产者,ClassPrinter作为字节码消费者
  28. ClassPrinter cp = new ClassPrinter();
  29. ClassReader cr = new ClassReader( "java.lang.Runnable");
  30. cr.accept(cp, 0);

输出:

  1. java/lang/Runnable extends java/lang/Object {
  2. run()V
  3. }

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

  1. package pkg;
  2. public interface Comparable extends Mesurable {
  3. int LESS = - 1;
  4. int EQUAL = 0;
  5. int GREATER = 1;
  6. int compareTo(Object o);
  7. }

test.java内容:

  1. ClassWriter cw = new ClassWriter( 0);
  2. cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "pkg/Comparable", null, "java/lang/Object", new String[] { "pkg/Mesurable" });
  3. cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I", null, new Integer(- 1)).visitEnd();
  4. cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I", null, new Integer( 0)).visitEnd();
  5. 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();
  6. byte[] b = cw.toByteArray();

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

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

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

  1. class StubClassLoader extends ClassLoader {
  2. @Override
  3. protected Class findClass(String name) throws ClassNotFoundException {
  4. if (name.endsWith( "_Stub")) {
  5. ClassWriter cw = new ClassWriter( 0);
  6. ...
  7. byte[] b = cw.toByteArray();
  8. return defineClass(name, b, 0, b.length);
  9. }
  10. return super.findClass(name);
  11. }
  12. }


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

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

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

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

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

具体架构图如下如所示,

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

 

这里的ChangeVersionAdapter继承自ClassVisitor,它没做其他过滤,仅仅重写了visit方法,过滤出类中的方法并指定方法的版本号(v1.5)。
  1. public class ChangeVersionAdapter extends ClassVisitor {
  2. public ChangeVersionAdapter(ClassVisitor cv) {
  3. super(ASM4, cv);
  4. }
  5. @Override
  6. public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  7. cv.visit(V1_5, access, name, signature, superName, interfaces);
  8. }
  9. }

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

图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字节码进行转换并加载的方法。

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

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

通过在visitMethod或者visitField方法中返回null的方式就能实现删除class中的方法或者变量的需求。如下实例,通过传入名字和描述删除class中的相应方法。
  1. public class RemoveMethodAdapter extends ClassVisitor {
  2. private String mName;
  3. private String mDesc;
  4. public RemoveMethodAdapter(ClassVisitor cv, String mName, String mDesc) {
  5. super(ASM4, cv);
  6. this.mName = mName;
  7. this.mDesc = mDesc;
  8. }
  9. @Override
  10. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
  11. if (name.equals(mName) && desc.equals(mDesc)) {
  12. // do not delegate to next visitor -> this removes the method
  13. return null;
  14. }
  15. return cv.visitMethod(access, name, desc, signature, exceptions);
  16. }
  17. }

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

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

  1. public class AddFieldAdapter extends ClassVisitor {
  2. private int fAcc;
  3. private String fName;
  4. private String fDesc;
  5. private boolean isFieldPresent;
  6. public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) {
  7. super(ASM4, cv);
  8. this.fAcc = fAcc;
  9. this.fName = fName;
  10. this.fDesc = fDesc;
  11. }
  12. @Override
  13. public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
  14. if (name.equals(fName)) {
  15. isFieldPresent = true;
  16. }
  17. return cv.visitField(access, name, desc, signature, value);
  18. }
  19. @Override
  20. public void visitEnd() {
  21. if (!isFieldPresent) {
  22. FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
  23. if (fv != null) {
  24. fv.visitEnd();
  25. }
  26. }
  27. cv.visitEnd();
  28. }
  29. }

7、classVisitor过滤(转换)链

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

图2.8过滤链

 

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

  1. public class MultiClassAdapter extends ClassVisitor { protected ClassVisitor[] cvs;
  2. public MultiClassAdapter(ClassVisitor[] cvs) {
  3. super(ASM4);
  4. this.cvs = cvs;
  5. }
  6. @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
  7. for (ClassVisitor cv : cvs) {
  8. cv.visit(version, access, name, signature, superName, interfaces);
  9. }
  10. }
  11. ...
  12. }

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:

  1. public class RewriterAgent {
  2. public static void agentmain(String agentArgs, Instrumentation instrumentation){ //代理类入口函数
  3. premain(agentArgs, instrumentation);
  4. }
  5. public static void premain(String agentArgs, Instrumentation instrumentation){ //代理类入口函数Java SE6
  6. try{
  7. DexClassTransformer dexClassTransformer = new DexClassTransformer(); //继承自ClassFileTransformer的自定义类,多class文件的加载
  8. instrumentation.addTransformer(dexClassTransformer, true);
  9. }
  10. catch(Throwable ex){
  11. System.out.println( "Agent startup error");
  12. throw new RuntimeException(ex);
  13. }
  14. }
  15. private static final class DexClassTransformer implements ClassFileTransformer{
  16. private Log log;
  17. private final Map classVisitors;
  18. public byte[] transform(ClassLoader classLoader, String className, Class clazz, ProtectionDomain protectionDomain, byte bytes[]) throws IllegalClassFormatException{
  19. ClassVisitorFactory factory = (ClassVisitorFactory)classVisitors.get(className); //根据给定className匹配相应class的工厂对象
  20. if(factory != null){
  21. if(clazz != null && !factory.isRetransformOkay()){
  22. return null;
  23. }
  24. try{
  25. ClassReader cr = new ClassReader(bytes); //生产者
  26. ClassWriter cw = new PatchedClassWriter( 3, classLoader); //消费者 自定义的ClassWriter
  27. ClassAdapter adapter = factory.create(cw); //过滤器
  28. cr.accept(adapter, 4);
  29. return cw.toByteArray();
  30. }
  31. catch(SkipException ex) { }
  32. catch(Exception ex){
  33. throw ex;
  34. }
  35. }
  36. return null;
  37. }
  38. public DexClassTransformer() throws URISyntaxException{
  39. classVisitors = new HashMap<String, ClassVisitorFactory>(){ //将需要转换的类放到map中,Map中为不同的类指定相应的工厂对象
  40. private static final long serialVersionUID = 1L;
  41. {
  42. put( "java/lang/ProcessBuilder", new ClassVisitorFactory( true){ //实现ClassVisitorFactory并重写create方法
  43. public ClassAdapter create(ClassVisitor cv){
  44. return RewriterAgent.createProcessBuilderClassAdapter(cv);
  45. }
  46. });
  47. }
  48. };
  49. }
  50. }
  51. private static ClassAdapter createProcessBuilderClassAdapter(ClassVisitor cw, Log log){
  52. return new ClassAdapter(cw){
  53. public MethodVisitor visitMethod(int access, String name, String desc, String signature, String exceptions[]){
  54. MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
  55. if( "start".equals(name)){
  56. System.out.println( "java.lang.ProcessBuilder实例的start方法被触发");
  57. }
  58. return mv;
  59. }
  60. };
  61. }
  62. }

自定义的ClassWriter:

  1. class PatchedClassWriter extends ClassWriter{
  2. private final ClassLoader classLoader;
  3. public PatchedClassWriter(int flags, ClassLoader classLoader){
  4. super(flags);
  5. this.classLoader = classLoader;
  6. }
  7. protected String getCommonSuperClass(String type1, String type2){ //返回共同的父类
  8. Class c;
  9. Class d;
  10. try{
  11. c = Class.forName(type1.replace( '/', '.'), true, classLoader);
  12. d = Class.forName(type2.replace( '/', '.'), true, classLoader);
  13. }
  14. catch(Exception e){
  15. throw new RuntimeException(e.toString());
  16. }
  17. if(c.isAssignableFrom(d))
  18. return type1;
  19. if(d.isAssignableFrom(c))
  20. return type2;
  21. if(c.isInterface() || d.isInterface())
  22. return "java/lang/Object";
  23. do
  24. c = c.getSuperclass();
  25. while(!c.isAssignableFrom(d));
  26. return c.getName().replace( '.', '/');
  27. }
  28. }

测试类AttachTest:

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

最后

以上就是落寞胡萝卜为你收集整理的转 : java asm库的原理与使用方法(一)的全部内容,希望文章能够帮你解决转 : java asm库的原理与使用方法(一)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(48)

评论列表共有 0 条评论

立即
投稿
返回
顶部