2.2.5移除类成员

前面例子中用来修改类版本号的方法也可以用在ClassVisitor接口中的其它方法上。例如,通过修改visitField和visitMethod方法中的access核name,你可以修改一个字段或者方法的访问修饰符和名称。更进一步,除了转发修改该参数的方法调用,你也可以选择不转发该方法调用,这样做的效果就是,对应的类元素将被移除。

 

例如,下面的类适配器将移除外部类和内部类,同时移除源文件的名称(修改过的类仍然是功能完整的,因为这些元素仅用作调试)。这主要是通过保留visit方法为空来实现。

public class RemoveDebugAdapter extends ClassAdapter {
         public RemoveDebugAdapter(ClassVisitor cv) {
                   super(cv);
         }
         @Override
         public void visitSource(String source, String debug) {
         }
         @Override
         public void visitOuterClass(String owner, String name, String desc) {
         }
         @Override
         public void visitInnerClass(String name, String outerName,
                   String innerName, int access) {
         }
}
上面的策略对字段和方法不起作用,因为visitField和visitMethod方法必须返回一个结果。为了移除一个字段或者一个方法,你不能转发方法调用,而是返回一个null。下面的例子,移除一个指定了方法名和修饰符的方法(单独的方法名是不足以确定一个方法,因为一个类可以包含多个相同方法名的但是参数个数不同的方法):
public class RemoveMethodAdapter extends ClassAdapter {
         private String mName;
         private String mDesc;
         public RemoveMethodAdapter(
                   ClassVisitor cv, String mName, String mDesc) {
                   super(cv);
                   this.mName = mName;
                   this.mDesc = mDesc;
         }
         @Override
         public 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 method
                            return null;
                   }
                   return cv.visitMethod(access, name, desc, signature, exceptions);
         }
}

2.2.6增加类成员

除了传递较少的方法调用,你也可以传递更多的方法调用,这样可以实现增加类元素。新的方法调用可以插入到原始方法调用之间,同时visitXxx方法调用的顺序必须保持一致(参看2.2.1)。

例如,如果你想给类增加一个字段,你需要在原始方法调用之间插入一个visitField调用,并且你需要将这个新的调用放置到类适配器的其中一个visit方法之中(这里的visit是指以visit打头的方法)。你不能在方法名为visit的方法中这样做,因为这样会导致后续对visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法的调用,这样做是无效的。同样,你也不能将对visitField方法的调用放置到visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法中。可能的位置是visitInnerClass,visitField,visitMethod和visitEnd方法。

如果你将这个调用放置到visitEnd中,字段总会被添加,除非你添加了显示的条件,因为这个方法总是会被调用。如果你把它放置到visitField或者visitMethod中,将会添加好几个字段,因为对原始类中每个字段或者方法的调用都会导致添加一个字段。两种方案都能实现,如何使用取决于你的需要。例如,你恶意增加一个单独的counter字段,用来统计对某个对象的调用次数,或者针对每个方法,添加一个字段,来分别统计对每个方法的调用。

注意:事实上,添加成员的唯一正确的方法是在visitEnd方法中增加额外的调用。同时,一个类不能包含重复的成员,而确保新添加的字段是唯一的方法就是比较它和已经存在的成员,这只能在所有成员都被访问之后来操作,例如在visitEnd方法中。程序员一般不大可能会使用自动生成的名字,如_counter$或者_4B7F_可以避免出现重复的成员,这样就不需要在visitEnd中添加它们。注意,如在第一章中讲的,tree API就不会存在这样的限制,使用tree API就可以在转换的任何时间点添加新成员。

 

为了展示上面的讨论,下面是一个类适配器,用来给一个类增加一个字段,除非这个字段已经存在:

public class AddFieldAdapter extends ClassAdapter {
         private int fAcc;
         private String fName;
         private String fDesc;
         private boolean isFieldPresent;
         public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
                   String fDesc) {
                   super(cv);
                   this.fAcc = fAcc;
                   this.fName = fName;
                   this.fDesc = fDesc;
         }
         @Override
         public 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);
         }
         @Override
         public void visitEnd() {
                   if (!isFieldPresent) {
                            FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
                            if (fv != null) {
                                     fv.visitEnd();
                            }
                   }
                   cv.visitEnd();
         }
}

这个字段是在visitEnd方法中添加的。重写visitField方法不是为了修改已经存在的字段,而是为了检测我们希望添加的字段是否已经存在。注意,在调用fv.visitEnd之前,我们测试了fv是否为空,如我们前面所讲,一个class visitor的visitField方法可以返回null。

 

2.2.7转换链

到目前为止,我们看到了一些有ClassReader,一个类适配器和ClassWriter组成的转换链。当然,也可以将多个类适配器连接在一起,来实现更复杂的转换链。链接多个类适配器运行你组合多个独立的类转换,以实现更复杂的转换。注意,一个转换链条没必要是线性的,你可以编写一个ClassVisitor,然后同时转发所有的方法调用给多个ClassVisitor:

public class MultiClassAdapter implements ClassVisitor {
         protected ClassVisitor[] cvs;
         public MultiClassAdapter(ClassVisitor[] cvs) {
                   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);
                   }
         }
         ...
}

相对地,多个类适配器也可以将方法调用都委托给相同的ClassVisitor(这需要额外的小心,以确保visit和visitEnd方法只被调用一次)。如图2.8这样的转换链也是可能地。

图2.8:一个复杂的转换链