├── .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
--------------------------------------------------------------------------------