问题的提出
在大部分情况下,需要多重继承往往意味着糟糕的设计。但在处理一些遗留项目的时候,多重继承可能是我们能做出的选择中代价最小的。由于 Java 语言本身不支持多重继承,这常常会给我们带来麻烦,最后的结果可能就是大量的重复代码。本文试图使用 ASM 框架来解决这一问题。在扩展类的功能的同时,不产生任何重复代码。
考虑如下的实际情况:有一组类,名为 SubClass1、SubClass2、SubClass3 和 SubClass4,它们共同继承了同一个父类 SuperClass。现在,我们需要这组类中的一部分,例如 SubClass1 和 SubClass2,这两个类还要实现另外两个接口,它们分别为:IFibonacciComputer 和 ITimeRetriever。然而,这两个接口已经有了各自的实现类 FibonacciComputer 和 TimeRetriever。并且这两个类的实现逻辑就是我们想要的,我们不想做任何改动,只希望在 SubClass1 和 SubClass2 两个类中包含这些实现逻辑。
它们的结构如图 1 所示:
由于 SubClass1,SubClass2 已经继承了 SuperClass,所以我们无法让它们再继承 FibonacciComputer 或 TimeRetriever。
所以,想要它们再实现 IFibonacciComputer 和 ITimeRetriever 这两个接口,必然会产生重复代码。
下面,我们就使用 ASM 来解决这个问题。
回页首
Java class 文件格式以及类加载器介绍
在后面的内容中,需要对 Java class 文件格式以及类加载器的知识有一定的了解,所以这里先对这些内容做一个简单介绍:
class 文件格式
Java class 文件的结构如图 2 所示(图中“*”表示出现 0 次或任意多次):
详细说明如下:
注:Modifiers,This Class,Super Class 和 Interfaces 这四项的和就是一个类的声明部分。
类装载器介绍
类装载器负责查找并装载类。每个类在被使用之前,都必须先通过类装载器装载到 Java 虚拟机当中。Java 虚拟机有两种类装载器 :
启动类装载器是 Java 虚拟机实现的一部分,每个 Java 虚拟机都必须有一个启动类装载器,它知道怎么装载受信任的类,比如 Java API 的 class 文件。
用户自定义装载器是普通的 Java 对象,它的类必须派生自 java.lang.ClassLoader 类。ClassLoader 类中定义的方法为程序提供了访问类装载机制的接口。
回页首
ASM 简介以及编程模型
ASM 简介
ASM 是一个可以用来生成\转换和分析 Java 字节码的代码库。其他类似的工具还有 cglib、serp和 BCEL等。相较于这些工具,ASM 有以下的优点 :
编程模型
ASM 提供了两种编程模型:
下文中,我们将只使用 Core API,因此我们只介绍与其相关的类。
Core API 中操纵字节码的功能基于 ClassVisitor 接口。这个接口中的每个方法对应了 class 文件中的每一项。Class 文件中的简单项的访问使用一个单独的方法,方法参数描述了这个项的内容。而那些具有任意长度和复杂度的项,使用另外一类方法,这类方法会返回一个辅助的 Visitor 接口,通过这些辅助接口的对象来完成具体内容的访问。例如 visitField 方法和 visitMethod 方法,分别返回 FieldVisitor 和 MethodVisitor 接口的对象。
清单 1 为 ClassVisitor 中的方法列表:
public interface ClassVisitor { // 访问类的声明部分 void visit(int version, int access, String name, String signature,String superName, String[] interfaces); // 访问类的代码 void visitSource(String source, String debug); // 访问类的外部类 void visitOuterClass(String owner, String name, String desc); // 访问类的注解 AnnotationVisitor visitAnnotation(String desc, boolean visible); // 访问类的属性 void visitAttribute(Attribute attr); // 访问类的内部类 void visitInnerClass(String name, String outerName, String innerName,int access); // 访问类的字段 FieldVisitor visitField(int access, String name, String desc, String signature, Object value); // 访问类的方法 MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions); // 访问结束 void visitEnd(); } |
ClassVisitor 接口中的方法在被调用的时候是有严格顺序的,其顺序如清单 2 所示(其中“?”表示被调用 0 次或 1 次。“*”表示被调用 0 次或任意多次):
visit visitSource? visitOuterClass? ( visitAnnotation| visitAttribute)* ( visitInnerClass| visitField| visitMethod)* visitEnd |
这就是说,visit 方法必须最先被调用,然后是最多调用一次 visitSource 方法,然后是最多调用一次 visitOuterClass 方法。然后是 visitAnnotation 和 visitAttribute 方法以任意顺序被调用任意多次。再然后是以任任意顺序调用 visitInnerClass ,visitField 或 visitMethod 方法任意多次。最终,调用一次 visitEnd 方法。
ASM 提供了三个基于 ClassVisitor 接口的类来实现 class 文件的生成和转换:
通常情况下,它们是组合起来使用的。
下面举一个简单的例子:假设现在需要对 class 文件的版本号进行修改,将其改为 Java 1.5。操作方法如下:
明白这些参数的含义之后,修改就很容易,只需要在调用 cv.visit 的时候,将 version 参数指定为 Opcodes.V1_5 即可(Opcodes 是 ASM 中的一个类),其他参数不加修改原样传递。这样,经过该 ClassAdapter 过滤后的类的版本号就都是 Java 1.5 了。
代码片段如清单 3 所示:
… // 使用 ChangeVersionAdapter 修改 class 文件的版本 ClassReader cr = new ClassReader(className); ClasssWriter cw = new ClassWriter(0); // ChangeVersionAdapter 类是我们自定义用来修改 class 文件版本号的类 ClassAdapter ca = new ChangeVersionAdapter (cw); cr.accept(ca, 0); byte[] b2 = cw.toByteArray(); … |
图 3 是相应的 Sequence 图:
代码示例
在了解了以上的知识之后再回到我们刚开始提出的问题中,我们希望 SubClass1 和 SubClass2 在继承自 SuperClass 的同时还要实现 IFibonacciComputer 以及 ITimeRetriever 两个接口。
为了后文描述方便,这里先确定三个名词:
如果只能在源代码级别进行修改,我们能做的仅仅是将实现类的代码拷贝进待增强类。(当然,有稍微好一点的做法在每一个待增强类中包含一个实现类,以组合的方式实现接口。但这仍然不能避免多个待增强类中的代码重复。)
在学习了 ASM 之后,我们可以直接从字节码的层次来进行修改。回忆一下上文中的内容:使用 ClassWrite 可以直接创建类的字节码,不同的方法创建了 class 文件的不同部分,尤其重要的是以下几个方法:
所以,现在我们可以直接从字节码的层次完成这一需求:动态的创建一个新的类(即增强类)继承自待增强类,同时在该类中,将实现类的实现方法添加进来。
完整的实现逻辑如下:
下面是代码示例与讲解:
首先需要修改 SubClass1 以及 SubClass2 两个类,使其声明实现 IFibonacciComputer 和 ITimeRetriever 这两个接口。由于这两个类并没有真正的包含实现接口的代码,所以它们现在必须标记为抽象类。修改后的类结构如图 4 所示:
然后创建以下几个类:
下面,我们来逐一实现这些类:
AddImplementClassAdapter: 首先在构造方法中,我们需要记录下待增强类的 Class 对象,增强类的类名,实现类的 Class 对象,以及一个 ClassWriter 对象,该构造方法代码如清单 4 所示:
public AddImplementClassAdapter( ClassWriter writer, String enhancedClassName,Class<?> targetClass, Class<?>... implementClasses) { super(writer); this.classWriter = writer; this.implementClasses = implementClasses; this.originalClassName = targetClass.getName(); this.enhancedClassName = enhancedClassName; } |
在 visit 方法中需要完成增强类声明部分的创建,增强类继承自待增强类。该方法代码如清单 6 所示:
// 通过 visit 方法完成增强类的声明部分 public void visit(int version, int access, String name, String signature,String superName, String[] interfaces) { cv.visit(version, Opcodes.ACC_PUBLIC, // 将 Java 代码中类的名称替换为虚拟机中使用的形式 enhancedClassName.replace('.', '/'), signature, name,interfaces); } |
visitMethod 方法中需要对构造方法做单独处理,因为 class 文件中的构造方法与源代码中的构造方法有三点不一样的地方:
鉴于这些原因,增强类的构造方法需要在待增强类构造方法的基础上进行修改。修改的内容就是对于父构造方法的调用,因为增强类和待增强类的父类是不一样的。
visitMethod 方法中需要判断如果是构造方法就通过 ModifyInitMethodAdapter 修改构造方法。其他方法直接返回 null 丢弃(因为增强类已经从待增强类中继承了这些方法,所以这些方法不需要在增强类中再出现一遍),该方法代码如清单 7 所示:
public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) { if (INTERNAL_INIT_METHOD_NAME.equals(name)) { // 通过 ModifyInitMethodAdapter 修改构造方法 MethodVisitor mv = classWriter.visitMethod(access, INTERNAL_INIT_METHOD_NAME, desc, signature, exceptions); return new ModifyInitMethodAdapter(mv, originalClassName); } return null; } |
最后,在 visitEnd 方法,使用 ImplementClassAdapter 与 ClassWriter 将实现类的内容添加到增强类中,最后再调用 visitEnd 方法表明增强类已经创建完成:
public void visitEnd() { for (Class<?> clazz : implementClasses) { try { // 逐个将实现类的内容添加到增强类中。 ClassReader reader = new ClassReader(clazz.getName()); ClassAdapter adapter = new ImplementClassAdapter(classWriter); reader.accept(adapter, 0); } catch (IOException e) { e.printStackTrace(); } } cv.visitEnd(); } |
ImplementClassAdapter:该类对实现类进行过滤。
首先在 visit 方法中给于空实现将类的声明部分过滤掉,代码如清单 8 所示:
public void visit(int version, int access, String name, String signature,String superName, String[] interfaces) { // 空实现,将该部分内容过滤掉 } |
然后在 visitMethod 中,将构造方法过滤掉,对于其他方法,调用 ClassVisitor#visitMethod 进行访问。由于这里的 ClassVisitor 是一个 ClassWriter,这就相当于在增强类中创建了该方法,代码如清单 9 所示:
public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) { // 过滤掉实现类中的构造方法 if (AddImplementClassAdapter.INTERNAL_INIT_METHOD_NAME.equals(name)){ return null; } // 其他方法原样保留 return cv.visitMethod(access, name, desc, signature, exceptions); } |
ModifyInitMethodAdapter:上文中已经提到,ModifyInitMethodAdapter 是用来对增强类的构造方法进行修改的。MethodAdapter 中的 visitMethodInsn 是对方法调用指令的访问。该方法的参数含义如下:
所以,我们需要将对于待增强类父类构造方法的调用改为对于待增强类构造方法的调用(因为增强类的父类就是待增强类),其代码如清单 10 所示:
/** 专门用来修改构造方法的方法适配器 */ public class ModifyInitMethodAdapter extends MethodAdapter { private String className; public ModifyInitMethodAdapter(MethodVisitor mv, String name) { super(mv); this.className = name; } public void visitMethodInsn(int opcode, String owner, String name,String desc) { // 将 Java 代码中的类全限定名替换为虚拟机中使用的形式 if (name.equals(AddImplementClassAdapter.INTERNAL_INIT_METHOD_NAME)) { mv.visitMethodInsn(opcode, className.replace(".", "/"), name, desc); } } } |
SimpleClassLoader:该自定义类装载器通过提供一个 defineClass 方法来装载动态生成的增强类。方法的实现是直接调用父类的 defineClass 方法,其代码如清单 11 所示:
public class SimpleClassLoader extends ClassLoader { public Class<?> defineClass(String className, byte[] byteCodes) { // 直接通过父类的 defineClass 方法加载类的结构 return super.defineClass(className, byteCodes, 0, byteCodes.length); } } |
EnhanceException:这是一个异常包装类,其中包含了待增强类和实现类的信息,其逻辑很简单,代码如清单 12 所示:
/** 异常类 */ public class EnhanceException extends Exception { private Class<?> enhanceClass; private Class<?> [] implementClasses; // 异常类构造方法 public EnhanceException(Exception ex,Class<?> ec,Class<?>... imClazz){ super(ex); this.enhanceClass = ec; this.implementClasses = imClazz; } public Class<?> getEnhanceClass() { return enhanceClass; } public Class<?>[] getImplementClasses() { return implementClasses; } } |
EnhanceFactory:最后,通过 EnhanceFactory 提供对外调用接口,调用接口有两个:
public static <T> Class<T> addImplementation(
Class<T> clazz,Class<?>... implementClasses)
public static <T> T newInstance(Class<T> clazz,
Class<?>... impls)
为了方便使用,这两个方法都使用了泛型。它们的参数是一样的:第一个参数都是待增强类的 Class 对象,后面是任意多个实现类的 Class 对象,返回的类型和待增强类一致,用户在获取返回值之后不需要进行任何类型转换即可使用。
第一个方法创建出增强类的 Class 对象,并通过自定义类加载器加载,其代码如清单 13 所示:
/** 静态工具方法,在待增强类中加入实现类的内容,返回增强类。 */ public static <T> Class<T> addImplementation(Class<T> clazz, Class<?>... implementClasses) throws EnhanceException { String enhancedClassName = clazz.getName() + ENHANCED; try { // 尝试加载增强类 return (Class<T>) classLoader.loadClass(enhancedClassName); } // 如果没有找到增强类,则尝试直接在内存中构建出增强类的结构 catch (ClassNotFoundException classNotFoundException) { ClassReader reader = null; try { reader = new ClassReader(clazz.getName()); } catch (IOException ioexception) { throw new EnhanceException(ioexception, clazz, implementClasses); } ClassWriter writer = new ClassWriter(0); // 通过 AddImplementClassAdapter 完成实现类内容的织入 ClassVisitor visitor = new AddImplementClassAdapter( enhancedClassName, clazz, writer, implementClasses); reader.accept(visitor, 0); byte[] byteCodes = writer.toByteArray(); Class<T> result = (Class<T>) classLoader.defineClass( enhancedClassName, byteCodes); return result; } } |
第二个方法先调用前一个方法,获取 增强类
的 Class
对象,然后使用反射创建实例,其代码如清单 14 所示:
/** 通过待增强类和实现类,得到增强类的实例对象 */ public static <T> T newInstance(Class<T> clazz, Class<?>... impls) throws EnhanceException { Class<T> c = addImplementation(clazz, impls); if (c == null) { return null; } try { // 通过反射创建实例 return c.newInstance(); } catch (InstantiationException e) { throw new EnhanceException(e, clazz, impls); } catch (IllegalAccessException e) { throw new EnhanceException(e, clazz, impls); } } |
下面是测试代码,先通过 EnhanceFactory 创建增强类的实例,然后就可以像普通对象一样的使用,代码如清单 15 所示:
// 不用 new 关键字,而使用 EnhanceFactory.newInstance 创建增强类的实例 SubClass1 obj1 = EnhanceFactory.newInstance(SubClass1.class, TimeRetriever.class,FibonacciComputer.class); // 调用待增强类中的方法 obj1.methodInSuperClass(); obj1.methodDefinedInSubClass1(); // 调用实现类中的方法 System.out.println("The Fibonacci number of 10 is "+obj1.compute(10)); System.out.println("Now is :"+obj1.tellMeTheTime()); System.out.println("--------------------------------------"); // 对于 SubClass2 的增强类实例的创建也是一样的 SubClass2 obj2 = EnhanceFactory.newInstance(SubClass2.class, TimeRetriever.class,FibonacciComputer.class); // 调用待增强类中的方法 obj2.methodInSuperClass(); obj2.methodDefinedInSubClass2(); // 调用实现类中的方法 System.out.println("The Fibonacci number of 10 is "+obj1.compute(10)); System.out.println("Now is :"+obj1.tellMeTheTime()); |
这里,我们演示了使用 ASM 创建一个新的类,并且修改该类中的内容的方法。而这一切都是在运行的环境中动态生成的,这一点相较于源代码级别的实现有以下的好处:
需要注意的是,这里我们并没有真正的实现“多重继承”,由于 class 文件格式的限制,我们也不可能真正实现“多重继承”,我们只是在一个类中包含了多个实现类的内容而已。但是,如果你使用增强类的实例通过 instanceof 之类的方法来判断它是否是实现类的实例的时候,你会得到 false,因为增强类并没有真正的继承自实现类。
另外,为了让演示代码足够的简单,对于这个功能的实现还存在一些问题,例如:
不过,在理解了上文中的知识之后,这些问题也都是可以解决的。
作为一个可以操作字节码的工具而言,ASM 的功能远不止于此。它还可以用来实现 AOP,实现性能监测,方法调用统计等功能。通过 Google,可以很容易的找到这类文章。
示例代码包含在 ASM_Demo.zip 中,该文件中包含了上文中提到的所有代码。
该 zip 文件为 eclipse 项目的归档文件。可以通过 Eclipse 菜单导入至 Eclipse 中,导入方法:File -> Import … -> Existing Projects into Workspace, 然后选择该 zip 文件即可。
想要编译该项目,还需要 ASM 框架的 jar 包。请在以下地址下载 ASM 框架:http://forge.ow2.org/projects/asm/
目前该框架正式版的版本号为:3.3.1。
下载该框架归档文件后解压缩, 并通过 eclipse 将 asm-all-3.3.1.jar(可能是其他版本号)添加到项目编译的类路径中即可。
代码中包含的 Main 类,是一个包含了 main 方法的可执行类,在 eclipse 中运行该类即可看到运行结果。