├── .gitignore ├── README.md ├── pom.xml └── src └── main ├── java └── bg │ └── bozho │ └── quickfix │ ├── MethodBodyReplacementAgent.java │ ├── MethodBodyTransformer.java │ └── ReplaceMethod.java └── resources └── META-INF └── MANIFEST.MF /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.classpath 3 | /.project 4 | /.settings 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | quickfix 2 | ======== 3 | 4 | A java agent that allows for runtime replacement of methods. 5 | 6 | When to use 7 | ======== 8 | 9 | Whenever you need to quickly replace a method of a 3rd-party library with your implementation 10 | 11 | How to use 12 | ======== 13 | 14 | 1. Annotate your replacement method with @ReplaceMethod and specify the target class and method to be replaced 15 | 2. Add method-replacements.txt file to the root of your classpath, containing a list of classes that contain @ReplaceMethod 16 | 3. Run your jvm with -javaagent:/path/to/quickfix.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | net.bozho 5 | quickfix 6 | 0.0.1-SNAPSHOT 7 | 8 | 9 | org.javassist 10 | javassist 11 | 3.22.0-GA 12 | 13 | 14 | 15 | 16 | 17 | org.apache.maven.plugins 18 | maven-compiler-plugin 19 | 2.3.2 20 | 21 | 1.8 22 | 1.8 23 | 24 | 25 | 26 | org.apache.maven.plugins 27 | maven-jar-plugin 28 | 3.0.2 29 | 30 | 31 | src/main/resources/META-INF/MANIFEST.MF 32 | 33 | 34 | false 35 | true 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/bg/bozho/quickfix/MethodBodyReplacementAgent.java: -------------------------------------------------------------------------------- 1 | package bg.bozho.quickfix; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.InputStream; 5 | import java.io.InputStreamReader; 6 | import java.lang.instrument.Instrumentation; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class MethodBodyReplacementAgent { 11 | 12 | public static void premain(String agentArgs, Instrumentation instrumentation) { 13 | try { 14 | InputStream in = MethodBodyReplacementAgent.class.getClassLoader().getResourceAsStream("method-replacements.txt"); 15 | if (in == null) { 16 | throw new Exception("/method-replacements.txt is missing. Include it and list all classes that define method replacements"); 17 | } 18 | BufferedReader r = new BufferedReader(new InputStreamReader(in)); 19 | String line = null; 20 | List> loadedClasses = new ArrayList>(); 21 | while ((line = r.readLine()) != null) { 22 | loadedClasses.add(Class.forName(line)); 23 | } 24 | r.close(); 25 | for (Class c : loadedClasses) { 26 | MethodBodyTransformer.setupMethodRedefinition(c); 27 | } 28 | 29 | Class[] classes = new Class[MethodBodyTransformer.replacementMethods.size()]; 30 | int i = 0; 31 | for (String c : MethodBodyTransformer.replacementMethods.keySet()) { 32 | classes[i++] = Class.forName(c); 33 | } 34 | 35 | instrumentation.addTransformer(new MethodBodyTransformer(), true); 36 | instrumentation.retransformClasses(classes); 37 | } catch (Exception ex) { 38 | throw new RuntimeException(ex); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/bg/bozho/quickfix/MethodBodyTransformer.java: -------------------------------------------------------------------------------- 1 | package bg.bozho.quickfix; 2 | 3 | import java.lang.instrument.ClassFileTransformer; 4 | import java.lang.instrument.IllegalClassFormatException; 5 | import java.lang.reflect.Method; 6 | import java.security.ProtectionDomain; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import javassist.ClassPool; 11 | import javassist.CtClass; 12 | import javassist.CtMethod; 13 | import javassist.NotFoundException; 14 | 15 | public class MethodBodyTransformer implements ClassFileTransformer { 16 | 17 | public static Map> replacementMethods = new HashMap>(); 18 | 19 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, 20 | ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 21 | return replaceMethod(className); 22 | } 23 | 24 | public static byte[] replaceMethod(String className) { 25 | try { 26 | String fqn = className.replace('/', '.'); 27 | Map methods = replacementMethods.get(fqn); 28 | if (methods == null) { 29 | return null; 30 | } 31 | ClassPool pool = ClassPool.getDefault(); 32 | CtClass cc = pool.get(fqn); 33 | 34 | for (String methodName : methods.keySet()) { 35 | CtMethod m = cc.getDeclaredMethod(methodName); 36 | m.setBody(methods.get(methodName), null); 37 | } 38 | return cc.toBytecode(); 39 | } catch (Exception ex) { 40 | throw new RuntimeException(ex); 41 | } 42 | } 43 | 44 | public static void setupMethodRedefinition(Class clazz) { 45 | try { 46 | Method[] methods = clazz.getDeclaredMethods(); 47 | ClassPool pool = ClassPool.getDefault(); 48 | for (Method method : methods) { 49 | ReplaceMethod annotation = method.getAnnotation(ReplaceMethod.class); 50 | if (annotation == null) { 51 | continue; 52 | } 53 | if (annotation.targetClassName().isEmpty() && annotation.targetClass() == Object.class) { 54 | throw new IllegalStateException("Either targetClass or targetClassName must be defined for @ReplaceMethod"); 55 | } 56 | 57 | String targetClassName = annotation.targetClassName(); 58 | if (targetClassName.isEmpty()) { 59 | targetClassName = annotation.targetClass().getName(); 60 | } 61 | Map methodMap = replacementMethods.get(targetClassName); 62 | if (methodMap == null) { 63 | methodMap = new HashMap(); 64 | replacementMethods.put(targetClassName, methodMap); 65 | } 66 | 67 | CtClass cc = pool.get(clazz.getName()); 68 | CtMethod[] declaredMethods = cc.getDeclaredMethods(); 69 | for (CtMethod declared : declaredMethods) { 70 | if (declared.getName().equals(annotation.methodName()) 71 | && (annotation.argumentTypes().length == 0 72 | || parameterTypesMatch(declared.getParameterTypes(), annotation.argumentTypes()))) { 73 | methodMap.put(annotation.methodName(), declared); 74 | } 75 | } 76 | } 77 | } catch (NotFoundException ex) { 78 | throw new RuntimeException(ex); 79 | } 80 | } 81 | 82 | private static boolean parameterTypesMatch(CtClass[] parameterTypes, Class[] argumentTypes) { 83 | if (parameterTypes.length != argumentTypes.length) { 84 | return false; 85 | } 86 | for (int i = 0; i < parameterTypes.length; i ++) { 87 | if (!parameterTypes[i].getName().equals(argumentTypes[i].getName())) { 88 | return false; 89 | } 90 | } 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/bg/bozho/quickfix/ReplaceMethod.java: -------------------------------------------------------------------------------- 1 | package bg.bozho.quickfix; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(value=ElementType.METHOD) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ReplaceMethod { 11 | String targetClassName() default ""; 12 | Class targetClass() default Object.class; 13 | String methodName(); 14 | Class[] argumentTypes() default {}; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Premain-Class: bg.bozho.quickfix.MethodBodyReplacementAgent 2 | Agent-Class: bg.bozho.quickfix.MethodBodyReplacementAgent 3 | Can-Redefine-Classes: true 4 | Can-Retransform-Classes: true --------------------------------------------------------------------------------