技术开发 频道

Java深度历险之Java字节代码的操纵

  如上所示,一个类或接口的字节代码使用的是一种松散的组织结构,其中所包含的内容依次排列。对于可能包含多个条目的内容,如所实现的接口、域、方法和属性等,是以数组来表示的。而在数组之前的是该数组中条目的个数。不同的内容类型,有其不同的内部结构。对于开发人员来说,直接操纵包含字节代码的字节数组的话,开发效率比较低,而且容易出错。已经有不少的开源库可以对字节代码进行修改或是从头开始创建新的Java类的字节代码内容。这些类库包括ASM、cglib、serp和BCEL等。使用这些类库可以在一定程度上降低增强字节代码的复杂度。比如考虑下面一个简单的需求,在一个Java类的所有方法执行之前输出相应的日志。熟悉AOP的人都知道,可以用一个前增强(before advice)来解决这个问题。如果使用ASM的话,相关的代码如下:

ClassNode cn = new ClassNode();
cr.accept(cn,
0);
for (Object object : cn.methods) {    
   MethodNode mn
= (MethodNode) object;  
  
if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {        
      continue;    
   }    
   InsnList insns
= mn.instructions;    
   InsnList il
= new InsnList();  
   il.add(
new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));    
   il.add(
new LdcInsnNode("Enter method -> " + mn.name));  
   il.add(
new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));    
   insns.insert(il);  mn.maxStack
+= 3;
}
ClassWriter cw
= new ClassWriter(0);
cn.accept(cw);
byte[] b = cw.toByteArray();

  从ClassWriter就可以获取到包含增强之后的字节代码的字节数组,可以把字节代码写回磁盘或是由类加载器直接使用。上述示例中,增强部分的逻辑比较简单,只是遍历Java类中的所有方法并添加对System.out.println方法的调用。在字节代码中,Java方法体是由一系列的指令组成的。而要做的是生成调用System.out.println方法的指令,并把这些指令插入到指令集合的最前面。ASM对这些指令做了抽象,不过熟悉全部的指令比较困难。ASM提供了一个工具类ASMifierClassVisitor,可以打印出Java类的字节代码的结构信息。当需要增强某个类的时候,可以先在源代码上做出修改,再通过此工具类来比较修改前后的字节代码的差异,从而确定该如何编写增强的代码。

  对类文件进行增强的时机是需要在Java源代码编译之后,在JVM执行之前。比较常见的做法有:

  由IDE在完成编译操作之后执行。如Google App Engine的Eclipse插件会在编译之后运行DataNucleus来对实体类进行增强。

  在构建过程中完成,比如通过Ant或Maven来执行相关的操作。

  实现自己的Java类加载器。当获取到Java类的字节代码之后,先进行增强处理,再从修改过的字节代码中定义出Java类。

  通过JDK 5引入的java.lang.instrument包来完成。

0
相关文章