Java ASM中收集方法及替换调用的变异测试技术咨询
在Java ASM中实现方法调用替换用于变异测试
当然可以用Java ASM实现你需要的变异测试逻辑!ASM本身没有直接封装好的现成工具,但它提供了非常灵活的字节码操作API,完全可以自定义实现方法收集和调用替换的功能。下面我一步步给你讲怎么搞:
一、先收集类中的所有方法信息
要替换方法调用,首先得把目标类里的所有方法信息都收集起来,包括方法名、参数/返回类型描述符、访问权限这些。我们可以写一个自定义的ClassVisitor来完成这件事:
import org.objectweb.asm.ClassVisitor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; class MethodCollector extends ClassVisitor { private final String ownerClass; // 按方法名分组:存储同名称的所有重载方法 private final Map<String, List<MethodInfo>> nameToOverloads = new HashMap<>(); // 按方法描述符分组:存储同参数/返回类型的所有方法 private final Map<String, List<MethodInfo>> descToMethods = new HashMap<>(); public MethodCollector(int asmApiVersion, String ownerClass) { super(asmApiVersion); this.ownerClass = ownerClass; } @Override public org.objectweb.asm.MethodVisitor visitMethod( int access, String name, String descriptor, String signature, String[] exceptions) { MethodInfo methodInfo = new MethodInfo(access, name, descriptor); // 把方法加入到对应分组里 nameToOverloads.computeIfAbsent(name, k -> new ArrayList<>()).add(methodInfo); descToMethods.computeIfAbsent(descriptor, k -> new ArrayList<>()).add(methodInfo); return super.visitMethod(access, name, descriptor, signature, exceptions); } // 提供获取收集结果的方法 public Map<String, List<MethodInfo>> getNameToOverloads() { return nameToOverloads; } public Map<String, List<MethodInfo>> getDescToMethods() { return descToMethods; } // 内部类,用来存储方法的关键信息 public static class MethodInfo { public final int access; public final String name; public final String descriptor; public MethodInfo(int access, String name, String descriptor) { this.access = access; this.name = name; this.descriptor = descriptor; } } }
这个收集器会遍历类的所有方法,把它们按“方法名”和“方法描述符”分别分组——前者用来找重载方法,后者用来找参数/返回类型相同但名称不同的方法。
二、实现两种方法调用替换逻辑
接下来我们需要写一个自定义的MethodVisitor,用来拦截字节码中的方法调用指令,然后替换成我们想要的目标方法。这里分两种替换场景:
1. 替换为重载方法
当遇到方法调用时,从同名称的重载方法列表里选一个和原调用描述符不同的方法,替换掉原调用的描述符。
2. 替换为参数/返回类型相同但名称不同的方法
从同描述符的方法列表里选一个和原调用名称不同的方法,替换掉原调用的方法名。
下面是整合了两种逻辑的MethodVisitor实现:
import org.objectweb.asm.MethodVisitor; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.stream.Collectors; class MethodCallMutator extends MethodVisitor { private final String ownerClass; private final MethodCollector methodCollector; private final MutateStrategy mutateStrategy; // 定义两种变异策略 public enum MutateStrategy { REPLACE_WITH_OVERLOAD, REPLACE_WITH_SAME_DESC_DIFF_NAME } public MethodCallMutator(int asmApiVersion, MethodVisitor mv, String ownerClass, MethodCollector collector, MutateStrategy strategy) { super(asmApiVersion, mv); this.ownerClass = ownerClass; this.methodCollector = collector; this.mutateStrategy = strategy; } @Override public void visitMethodInsn( int opcode, String owner, String name, String descriptor, boolean isInterface) { // 这里只处理当前类的方法调用,如果需要处理其他类的调用,可以去掉这个判断 if (this.ownerClass.equals(owner)) { MethodCollector.MethodInfo replacement = findReplacementMethod(name, descriptor); if (replacement != null) { // 替换成目标方法的调用 super.visitMethodInsn(opcode, owner, replacement.name, replacement.descriptor, isInterface); return; } } // 找不到替换目标时,保留原方法调用 super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } private MethodCollector.MethodInfo findReplacementMethod(String originalName, String originalDesc) { List<MethodCollector.MethodInfo> candidates = null; switch (mutateStrategy) { case REPLACE_WITH_OVERLOAD: // 筛选出同名称但不同描述符的重载方法 candidates = methodCollector.getNameToOverloads().getOrDefault(originalName, Collections.emptyList()); candidates = candidates.stream() .filter(info -> !info.descriptor.equals(originalDesc)) .collect(Collectors.toList()); break; case REPLACE_WITH_SAME_DESC_DIFF_NAME: // 筛选出同描述符但不同名称的方法 candidates = methodCollector.getDescToMethods().getOrDefault(originalDesc, Collections.emptyList()); candidates = candidates.stream() .filter(info -> !info.name.equals(originalName)) .collect(Collectors.toList()); break; } // 变异测试通常随机选一个候选方法,这里用简单的随机逻辑 if (!candidates.isEmpty()) { return candidates.get(new Random().nextInt(candidates.size())); } return null; } }
三、关键注意事项
- 方法访问权限:替换时要注意目标方法的访问权限,比如不能把调用替换成私有方法(除非当前方法在同一个类里),否则运行时会抛出权限异常。可以在筛选候选方法时加上访问权限的判断(比如
!Modifier.isPrivate(info.access))。 - 调用类型匹配:要注意方法调用的 opcode(比如
INVOKESTATIC对应静态方法,INVOKEVIRTUAL对应实例方法),替换后的方法类型要和原调用一致,不能把静态方法调用替换成实例方法调用,反之亦然。 - 特殊方法处理:比如构造方法(
<init>)、静态初始化方法(<clinit>),这些方法通常不需要在变异测试中替换,可以在筛选时排除掉。 - 外部类方法调用:如果需要替换对其他类的方法调用,你需要先收集目标类的方法信息,再在替换逻辑中匹配对应的类。
内容的提问来源于stack exchange,提问作者knt5784
相关产品推荐
相关产品推荐

