support specify target class and method for injection

This commit is contained in:
金戟
2020-10-12 23:34:21 +08:00
parent bf90b5c3b9
commit 6c5bfb9c9b
8 changed files with 102 additions and 33 deletions

View File

@@ -4,6 +4,7 @@ import com.alibaba.testable.agent.constant.ConstPool;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.CollectionUtil;
import com.alibaba.testable.agent.util.StringUtil;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
@@ -31,7 +32,7 @@ public class SourceClassHandler extends BaseClassHandler {
List<MethodInfo> methods = new ArrayList<MethodInfo>();
for (MethodNode m : cn.methods) {
if (!ConstPool.CONSTRUCTOR.equals(m.name)) {
methods.add(new MethodInfo(m.name, m.desc));
methods.add(new MethodInfo(cn.name, m.name, m.desc));
}
}
Set<MethodInfo> memberInjectMethods = CollectionUtil.getCrossSet(methods, injectMethods);
@@ -48,13 +49,15 @@ public class SourceClassHandler extends BaseClassHandler {
do {
if (instructions[i].getOpcode() == Opcodes.INVOKESPECIAL) {
MethodInsnNode node = (MethodInsnNode)instructions[i];
if (cn.name.equals(node.owner) && memberInjectMethods.contains(new MethodInfo(node.name, node.desc))) {
if (cn.name.equals(node.owner) && memberInjectMethods.contains(new MethodInfo(cn.name, node.name, node.desc))) {
// it's a member method and an inject method for it exist
int rangeStart = getMemberMethodStart(instructions, i);
if (rangeStart >= 0) {
instructions = replaceMemberCallOps(cn, mn, instructions, rangeStart, i);
i = rangeStart;
}
} else if (ConstPool.CONSTRUCTOR.equals(node.name)) {
// it's a new operation
String newOperatorInjectMethodName = getNewOperatorInjectMethodName(newOperatorInjectMethods, node);
if (newOperatorInjectMethodName.length() > 0) {
int rangeStart = getConstructorStart(instructions, node.owner, i);
@@ -105,7 +108,7 @@ public class SourceClassHandler extends BaseClassHandler {
AbstractInsnNode[] instructions, int start, int end) {
String classType = ((TypeInsnNode)instructions[start]).desc;
String constructorDesc = ((MethodInsnNode)instructions[end]).desc;
String testClassName = cn.name + ConstPool.TEST_POSTFIX;
String testClassName = StringUtil.getTestClassName(cn.name);
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,
@@ -124,7 +127,7 @@ public class SourceClassHandler extends BaseClassHandler {
private AbstractInsnNode[] replaceMemberCallOps(ClassNode cn, MethodNode mn, AbstractInsnNode[] instructions,
int start, int end) {
MethodInsnNode method = (MethodInsnNode)instructions[end];
String testClassName = cn.name + ConstPool.TEST_POSTFIX;
String testClassName = StringUtil.getTestClassName(cn.name);
mn.instructions.insertBefore(instructions[start], new FieldInsnNode(GETSTATIC, testClassName,
ConstPool.TESTABLE_INJECT_REF, ClassUtil.toByteCodeClassName(testClassName)));
mn.instructions.insertBefore(instructions[end], new MethodInsnNode(INVOKEVIRTUAL, testClassName,

View File

@@ -5,14 +5,20 @@ package com.alibaba.testable.agent.model;
*/
public class MethodInfo {
private final String clazz;
private final String name;
private final String desc;
public MethodInfo(String name, String desc) {
public MethodInfo(String clazz, String name, String desc) {
this.clazz = clazz;
this.name = name;
this.desc = desc;
}
public String getClazz() {
return clazz;
}
public String getName() {
return name;
}
@@ -23,19 +29,21 @@ public class MethodInfo {
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
MethodInfo that = (MethodInfo)o;
return name.equals(that.name) && desc.equals(that.desc);
if (!clazz.equals(that.clazz)) { return false; }
if (!name.equals(that.name)) { return false; }
return desc.equals(that.desc);
}
@Override
public int hashCode() {
return 31 * name.hashCode() + desc.hashCode();
int result = clazz.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + desc.hashCode();
return result;
}
}

View File

@@ -5,6 +5,7 @@ import com.alibaba.testable.agent.handler.SourceClassHandler;
import com.alibaba.testable.agent.handler.TestClassHandler;
import com.alibaba.testable.agent.model.MethodInfo;
import com.alibaba.testable.agent.util.ClassUtil;
import com.alibaba.testable.agent.util.StringUtil;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
@@ -24,18 +25,20 @@ public class TestableClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classFileBuffer) {
if (isSystemClass(loader, className) || loadedClassNames.contains(className)) {
// Ignore system class and duplicate class
// Ignore system class and reloaded class
return null;
}
List<String> annotations = ClassUtil.getAnnotations(className);
List<String> testAnnotations = ClassUtil.getAnnotations(className + ConstPool.TEST_POSTFIX);
List<String> testAnnotations = ClassUtil.getAnnotations(StringUtil.getTestClassName(className));
try {
if (testAnnotations.contains(ConstPool.ENABLE_TESTABLE)) {
// it's a source class with testable enabled
loadedClassNames.add(className);
List<MethodInfo> injectMethods = ClassUtil.getTestableInjectMethods(className + ConstPool.TEST_POSTFIX);
List<MethodInfo> injectMethods = ClassUtil.getTestableInjectMethods(StringUtil.getTestClassName(className));
return new SourceClassHandler(injectMethods).getBytes(className);
} else if (annotations.contains(ConstPool.ENABLE_TESTABLE)) {
// it's a test class with testable enabled
loadedClassNames.add(className);
return new TestClassHandler().getBytes(className);
}

View File

@@ -31,6 +31,9 @@ public class ClassUtil {
private static final char TYPE_ARRAY = '[';
private static final Map<Character, String> TYPE_MAPPING = new HashMap<Character, String>();
private static final String TARGET_CLASS = "targetClass";
private static final String TARGET_METHOD = "targetMethod";
static {
TYPE_MAPPING.put(TYPE_BYTE, "java/lang/Byte");
TYPE_MAPPING.put(TYPE_CHAR, "java/lang/Character");
@@ -60,13 +63,17 @@ public class ClassUtil {
}
}
/**
* Get testable inject method from test class
* @param className test class name
*/
public static List<MethodInfo> getTestableInjectMethods(String className) {
try {
List<MethodInfo> methodInfos = new ArrayList<MethodInfo>();
ClassNode cn = new ClassNode();
new ClassReader(className).accept(cn, 0);
for (MethodNode mn : cn.methods) {
checkMethodAnnotation(methodInfos, mn);
checkMethodAnnotation(cn, methodInfos, mn);
}
return methodInfos;
} catch (Exception e) {
@@ -74,18 +81,30 @@ public class ClassUtil {
}
}
private static void checkMethodAnnotation(List<MethodInfo> methodInfos, MethodNode mn) {
private static void checkMethodAnnotation(ClassNode cn, List<MethodInfo> methodInfos, MethodNode mn) {
if (mn.visibleAnnotations == null) {
return;
}
for (AnnotationNode an : mn.visibleAnnotations) {
if (toDotSeparateFullClassName(an.desc).equals(ConstPool.TESTABLE_INJECT)) {
methodInfos.add(new MethodInfo(mn.name, mn.desc));
String targetClass = getAnnotationParameter(an, TARGET_CLASS, StringUtil.getSourceClassName(cn.name));
String targetMethod = getAnnotationParameter(an, TARGET_METHOD, mn.name);
methodInfos.add(new MethodInfo(toSlashSeparateName(targetClass), targetMethod, mn.desc));
break;
}
}
}
private static String getAnnotationParameter(AnnotationNode an, String key, String defaultValue) {
if (an.values != null) {
int i = an.values.indexOf(key);
if (i % 2 == 0) {
return (String)an.values.get(i+1);
}
}
return defaultValue;
}
public static List<Byte> getParameterTypes(String desc) {
List<Byte> parameterTypes = new ArrayList<Byte>();
boolean travelingClass = false;
@@ -122,8 +141,12 @@ public class ClassUtil {
}
}
private static String toSlashSeparateName(String name) {
return name.replace(ConstPool.DOT, ConstPool.SLASH);
}
public static String toByteCodeClassName(String className) {
return TYPE_CLASS + className.replace(ConstPool.DOT, ConstPool.SLASH) + CLASS_END;
return TYPE_CLASS + toSlashSeparateName(className) + CLASS_END;
}
public static String toDotSeparateFullClassName(String className) {

View File

@@ -1,10 +1,17 @@
package com.alibaba.testable.agent.util;
import com.alibaba.testable.agent.constant.ConstPool;
/**
* @author flin
*/
public class StringUtil {
/**
* repeat a text many times
* @param text content to repeat
* @param times count of repeating
*/
public static String repeat(String text, int times) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < times; i++) {
@@ -13,4 +20,20 @@ public class StringUtil {
return sb.toString();
}
/**
* get test class name from source class name
* @param sourceClassName source class name
*/
public static String getTestClassName(String sourceClassName) {
return sourceClassName + ConstPool.TEST_POSTFIX;
}
/**
* get source class name from test class name
* @param testClassName test class name
*/
public static String getSourceClassName(String testClassName) {
return testClassName.substring(0, testClassName.length() - ConstPool.TEST_POSTFIX.length());
}
}

View File

@@ -11,10 +11,4 @@ import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Documented
public @interface EnableTestable {
/**
* Whether use compile time code modification
*/
boolean withoutAgent() default false;
}

View File

@@ -11,10 +11,4 @@ import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Documented
public @interface EnableTestableInject {
/**
* Test class names
*/
String[] value();
}

View File

@@ -11,4 +11,25 @@ import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Documented
public @interface TestableInject {
/**
* only enable for invocation from specified methods
*/
String[] sourceMethods() default {};
/**
* only enable for invocation from specified test cases
*/
String[] testMethods() default {};
/**
* mock method of specified class instead of the class under test
*/
String targetClass() default "";
/**
* mock specified method instead of method with same name
*/
String targetMethod() default "";
}