问题的提出
http://bbs.itheima.com/thread-23776-1-1.html?fstgj
以前的学习网站,-全套java视频教程,需要的自己看下,可以去这个网站下载,下载视频免费,不需要注册和做什么任务
在大部分情况下,需要多重继承往往意味着糟糕的设计。但在处理一些遗留项目的时候,多重继承可能是我们能做出的选择中代价最小的。由于 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 中的方法列表:
清单 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 次或任意多次):
清单 2.ClassVisitor 中方法调用的顺序
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 所示:
清单 3. 使用 ASM 的代码示例
… // 使用 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 文件的不同部分,尤其重要的是以下几个方法:
所以,现在我们可以直接从字节码的层次完成这一需求:动态的创建一个新的类(即增强类)继承自待增强类,同时在该类中,将实现类的实现方法添加进来。
完整的实现逻辑如下: