- 
                Notifications
    
You must be signed in to change notification settings  - Fork 2
 
Home
Rewrite existing classes by grafting bytecode from Transplant classes. Transplants are plain java classes and function like templates or patches; annotations define interactions with the recipient class. The entire API consists of 4 annotations.
Transplants have complete access to the recipient class: Existing methods can be wrapped, changed or replaced entirely. Interfaces can be retrofitted and additional fields added. Update annotations on classes, methods and fields.
- graftt
 
In the interest of brevity.
- agent: the java agent responsible for applying transplants at load-time.
 - donor: transplants are occasionally referred to as donor, especially in the source.
 - 
fuse/fusing: merges transplant method with matching method in recipient. Implies 
@Graft.Fuse. - 
transplant (class): class annotated with 
@Graft.Recipient. Transplants bytecode to the recipient. - 
recipient (class): the target of 
@Graft.Recipient(target). 
Using the API revolves around writing a template representing the actual methods and
fields that should be copied over. Transplants can reference internal symbols by annotating them
with @Graft.Mock in the transplant.
Adding a custom toString() to a final class on the classpath can be as simple as:
@Graft.Recipient(Foo.class) // all transplants have a recipient
public class FooTransplant {
    @Graft.Mock // simulating Foo.toDebugString
    private String toDebugString() { 
        return null; // never called, return value is unimportant
    } 
    @Graft.Fuse // "fusing" (here: replacing) existing Foo::toString
    public String toString() {
        return "hello world: " + toDebugString();
    }
}- 
@Graft.Recipientspecifies which class to transplant to. - 
@Graft.Fusetransplants bytecode over to@Graft.Recipient, translating anyFooTransplantreferences toFoo. Call the original method at any time by invoking the method currently being fused; e.g. FusingFooTransplant::barwithFoo::bar, any call tobar()inside the transplant will point toFoo::bar$originalonce applied. - 
@Graft.Mockto keep the compiler happy when you need to reference fields or methods in the target class. Mocked references point to target class after transplant. - 
@Graft.Annotationsoverrides default configuration for removal and updating of annotations. The default behavior copies all annotations from the transplanted elements to the recipient. - Interfaces implemented by the transplant are added to the recipient.
 - All fields and methods, except those annotated with 
@Graft.Mock, are copied to recipient. 
For fused methods, the original method is renamed with "$original" suffixed to the name. The original method only exists if it was invoked by the transplant, otherwise the fused method replaces the original. Any annotations decorating the original methods are moved to the transplanted method.
It is good taste to add the -Transplant suffix to these classes.
A short, but mostly thorough description of each feature.
To copy a new field, it's enough to declare it:
@Graft.Recipient(Foo.class)
public class FooTransplant {
    private int randomNumber = 4;
}Only primitive fields can be initialized with a value. The below transplant will hence not work.
@Graft.Recipient(Foo.class)
public class FooTransplant {
    private ArrayList<String> yolo = new ArrayList<>(); // :(    
}It is sometimes convenient to change the modifiers of a field, such as removing
final or increasing the visibility. If Foo.bar is a private final field,
the transplant would take a form similar to:
@Graft.Recipient(Foo.class)
public class FooTransplant {
    @Graft.Fuse        // identifies field by name and type
    protected Bar bar; // private -> protected and remove final
}All modifiers on the field are replaced by the modifiers from the fused
field. Note that some modifiers can cause surrounding code to break,
e.g. adding final or static to a field.
It is not possible to change the type of a field, but if a transplant for the type exists, it can be used in its place:
@Graft.Recipient(Foo.class)
public class FooTransplant {
    @Graft.Fuse
    protected BarTransplant bar;
}In the above snippet - if field modifiers don't need to be updated on, @Graft.Mock
is recommended as it leaves the original field intact.
As with fields, methods are transplanted by default:
@Graft.Recipient(Foo.class)
public class FooTransplant {
    public void hello(String s) {
        System.out.println("hello " + s);
    }
}Wrap a method with @Graft.Fuse. The annotated method's signature must match
a method in the recipient class.
A fused method can invoke the original method by simply calling itself.
@Graft.Recipient(Foo.class)
public class FooTransplant {
    
    @Graft.Fuse    
    public void hello(String s) {
        long start = System.currentTimeMillis();
        hello(s); // calls original method Foo::hello
        long end = System.currentTimeMillis();    
        System.out.println("hello(s) executed in: " + (end - start));
    }
}Fusing a method without invoking the original method completely removes the original method from the recipient.
@Graft.Recipient(Foo.class)
public class FooTransplant {
    
    @Graft.Fuse // matching Foo::hello    
    public void hello(String s) {
        // never calling Foo.hello(s) - original is deleted
        System.out.println("good bye " + s);
    }
}Annotations on transplant class, fields and methods are copied over by default. No
Graft.* annotations are transplanted however.
@Graft.Recipient(Foo.class)
@MyAnnotation
public class FooTransplant {
    
    @MyAnnotation
    private int i;    
    
    @MyAnnotation
    @Graft.Fuse // <- matching Foo::otherField
    private int otherField;    
    @Graft.Fuse
    @MyAnnotation     
    public void hello(String s) {
        hello(s);
    }
    
    @MyAnnotation
    public void goodBye(String s) {
        System.out.println("good bye " + s);
    }
}Fused methods retain all annotations decorating the recipient's original method.
Removing SomeAnno from Foo:
@Graft.Recipient(Foo.class)
@Graft.Annotations(remove = SomeAnno.class)
public class FooTransplant {}Removing SomeAnno from field in Foo - note that Graft.Fuse is required:
@Graft.Recipient(Foo.class)
public class FooTransplant {
    @Graft.Fuse
    @Graft.Annotations(remove = SomeAnno.class)
    private byte[] someField;
}Removing SomeAnno from method in Foo - note that Graft.Fuse is required:
@Graft.Recipient(Foo.class)
public class FooTransplant {
    @Graft.Fuse
    @Graft.Annotations(remove = SomeAnno.class)
    private boolean someMethod() {
        return someMethod();    
    }
}@Graft.Annotations(overwrite = true) indicates that transplanted annotations
replace any matching annotation in the recipient:
@Graft.Recipient(Foo.class)
@Graft.Annotations(overwrite = true)
@SomeAnno(2)
public class FooTransplant {
    @Graft.Fuse
    @Graft.Annotations(overwrite = true)
    @SomeAnno(3)
    private byte[] someField;
    
    @Graft.Fuse
    @Graft.Annotations(overwrite = true)
    @SomeAnno(1)
    private boolean someMethod() {
        return someMethod();    
    }
}Any interfaces implemented by a transplant are added to the recipient.
@Graft.Recipient(Foo.class)
public class FooTransplant implements Point {
    @Graft.Mock private int x = 1;
    @Graft.Mock private int y = 2;
    @Override public int x() { return x; }
    @Override public int y() { return y; }
}In a situation where Foo already declares private non-public interface
methods with matching names - x() and y() - one option is to wrap the
original method:
@Graft.Recipient(Foo.class)
public class FooTransplant implements Point {
    @Graft.Fuse @Override
    public int x() { return x(); } // Foo.x() is non-public
    @Graft.Fuse @Override
    public int y() { return y(); } // Foo.y() is non-public
}Note that only interfaces are supported; changing the parent class is not supported.
Mocks serve two purposes:
- When there is a need to reference fields or methods in recipient.
 - Mocking is also a requirement for compilation to pass in the above case.
 
@Graft.Recipient(Foo.class)
public class FooTransplant {
    
    @Graft.Mock
    private void clean() { }
    
    @Graft.Mock
    public int lastResult;
    
    @Graft.Fuse
    public int execute() {
        clean();                // Foo.clean()
        lastResult = execute(); // Foo.execute()
        return lastResult;
    }
}Mocked fields may also annotate another transplant type:
@Graft.Mock // replaced with `Foo` after transplant
private FooTransplant foo;Mocked fields and methods have no effect on the recipient.
It is sometimes necessary to create additional placeholder classes for the transplants to compile - e.g. when a class isn't visible due to package visibility, but the class must still be referenced by a transplant.
Such classes are never transplanted as they're not a part of any transplant.
class InternalClass {
    public int something() {
        throw new IllegalStateException("i'm never invoked");
    }
}
@Graft.Recipient(Foo.class)
public class FooTransplant {
    @Graft.Mock
    private InternalClass internalStuff;
    @Graft.Fuse
    public int execute() {
        return internalStuff.something();
    }
}Within a transplant class, any references to transplants are translated to their recipient type. This way it is possible to invoke methods added through transplants.
// adding getName() to Bar
@Graft.Recipient(Bar.class)
public class BarTransplant {
    @Graft.Mock // mocking Bar::name for getName()
    private String name;    
    public String getName() {
        return name;    
    }
}
// holds Bars in a list
public class Foo {
    private List<Bar> children = new ArrayList<>();
    public void add(Bar bar) {
        children.add(bar);
    }
}
@Graft.Recipient(Foo.class)
public class FooTransplant {
    @Graft.Mock // mock for List<Bar> children
    private List<BarTransplant> children = new ArrayList<>();
    @Graft.Fuse // fusing with Foo::add(Bar)
    public void add(BarTransplant bar) { 
        System.out.println(bar.getName());
        children.add(bar);
    }
}- TODO: Donor's lambdas and inner classes are not transplanted.
 - TODO: Not yet possible to append to constructor or static initializer.
 - Transplanted fields are initialized to 
nullor0. Primitive fields can hold values, and string fields are eligible if the string points to a constant. - Parent class of recipient cannot be changed.
 - Not possible to remove fields, methods or interfaces. Methods can be replaced but not removed.
 - No support for multiple transplants denoting the same recipient; will fail if both
try to 
@Graft.Fusewith the same method. 
Additionally, supporting classes from JVM languages other than java is not supported; it should work for simple use-cases, but would not support any language features present in e.g. kotlin.
Kotlin support is considered.
Two means of grafting transplants are available out of the box:
- load-time java agent: transplanting before class reaches the classloader. Transformed classes exist only in memory and die when the application ends.
 - build-time maven plugin: transplanting during compilation. After compilation, resulting classes have been transformed by matching transplants.
 
Transplanting using the maven plugin and agent differ in which classes are eligible. The maven
plugin is restricted to only transplanting to classes under target/classes, while
the agent has free rein over where recipient classes originate from.
The use-case for transplanting with the maven plugin is to conditionally activate the plugin when building debug or similar variants. As the transformed class files are updated, this approach is also compatible with android and other environments where running a java agent isn't feasible.
The other use-case for the plugin is generate graftt.index files, used by the agent
to automate transplant discovery.
The agent works by transforming classes before they're loaded by the classloader.
There are three ways the agent registers transplants:
- Transplants listed in 
/graftt.indexfiles on the classpath are registered automatically. - By referencing the transplants directly, e.g. by calling
Class.forName("com.mycompany.transplant.MyLittleTransplant"). - From root directories and path/to/jar passed to agent: such transplants are not required to reside on the classpath.
 
During start-up, the agent reads all /graftt.index present on the classpath and registers
the transplants listed therein.
The most straightforward approach is to generate the index using the generate-index goal - it
is then enough to include the artifact as a dependency.
    <plugin>
        <groupId>net.onedaybeard.graftt</groupId>
        <artifactId>graftt-maven-plugin</artifactId>
        <version>${graftt.version}</version>
        <executions>
            <execution>
                <id>scan-for-transplants</id>
                <goals>
                    <goal>generate-index</goal>
                </goals>
            </execution>
        </executions>
    </plugin>Alternatively, graftt.index can be created manually too. It takes the following form:
# blank lines and lines starting with '#' are ignored.
# in real life, usually generated by: graftt-maven-plugin:generate-index
# qualified name of each transplant
net.onedaybeard.graftt.FooTransplant
net.onedaybeard.graftt.BarTransplant
When no arguments are passed to the agent, and no graftt.index exists for all transplants,
remaining transplants must be loaded by classloader in order to register them.
# all transplants live on classpath; registration by referencing transplants
java -javaagent:agent-${VERSION}.jar ...Once a Transplant is loaded by the agent, it is registered; the transplant is applied when its recipient class is loaded. It is therefore important to make sure that any local transplants are registered before the recipient class is loaded.
fun main(args : Array<String>) {
    FooTransplant::class
    BarTransplant::class
The cp argument specifies a , delimited list of paths. Each path must denote
an existing directory or .jar file.
All paths are scanned recursively for transplants prior to the application loading
any classes. As such, there is no need to explicitly instantiate transplants found
in cp paths.
# transplants from cp args are auto-registered
java -javaagent:agent-${VERSION}.jar=cp=/path1/to/dir,/path/to/jar ...The maven plugin provides the following goals:
- 
generate-index: generatesgraftt.index, automatically picked up by agent - 
transplant: applies transplants during build 
Scans target/classes for local transplants and records them in target/classes/graftt.index.
This file is used by the agent to register all listed transplants during start-up.
    <plugin>
        <groupId>net.onedaybeard.graftt</groupId>
        <artifactId>graftt-maven-plugin</artifactId>
        <version>${graftt.version}</version>
        <executions>
            <execution>
                <id>scan-for-transplants</id>
                <goals>
                    <goal>generate-index</goal>
                </goals>
            </execution>
        </executions>
    </plugin>As the agent scans the classpath for graftt.index files, the artifact must be included
as a runtime dependency.
The maven plugin applies transplants during the process-classes lifecycle phase.
Transplants found locally under target/classes are applied automatically.
Additional transplants can be passed as dependencies,
jars and root class directories.
Running with the default configuration - only applying transplants from target/classes:
<plugin>
    <groupId>net.onedaybeard.graftt</groupId>
    <artifactId>graftt-maven-plugin</artifactId>
    <version>${project.version}</version>
    <executions>
        <execution>
            <id>transplant</id>
            <goals>
                <goal>transplant</goal>
            </goals>
        </execution>
    </executions>
</plugin>A path must resolve to an existing jar file or path. Directories and jar files are searched recursively for Transplants.
If the <path> element denotes a directory, only .class files are inspected. Any .jar
files that happen to be under the directory tree are ignored.
<plugin>
    <groupId>net.onedaybeard.graftt</groupId>
    <artifactId>graftt-maven-plugin</artifactId>
    <version>${graftt.version}</version>
    <executions>
        <execution>
            <id>graftt</id>
            <goals>
                <goal>graftt</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <!-- paths to root of class directories or .jar files -->
        <paths>
            <path>/path/to/transplants</path>
            <path>/path/to/transplants.jar</path>
        </paths>
    </configuration>
</plugin>Transplants can also come from maven dependencies. The plugin inspects
all transplants from its own dependencies. For artifact resolution
to work, the dependency (unfortunately) also has to be added to the pom.xml's
dependencies - otherwise the referenced jar files aren't resolved.
    <dependencies>
        <!-- resolving referenced .jar file -->
        <dependency>
            <groupId>com.mydomain.foo</groupId>
            <artifactId>transplants</artifactId>
            <version>${foo.version}</version>
            <!-- don't need transplants at runtime -->
            <scope>provided</scope>
        </dependency>
    </dependencies>
<build>
    <plugins>
        <plugin>
            <groupId>net.onedaybeard.graftt</groupId>
            <artifactId>graftt-maven-plugin</artifactId>
            <version>${graftt.version}</version>
            <executions>
                <execution>
                    <id>graftt</id>
                    <goals>
                        <goal>graftt</goal>
                    </goals>
                </execution>
            </executions>
            <dependencies>
                <!-- registering transplants with plugin -->
                <dependency>
                    <groupId>com.mydomain.foo</groupId>
                    <artifactId>transplants</artifactId>
                    <version>${foo.version}</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>The maven plugin rewrites classes under the active project's target/classes. Any
Transplant classes in the directory are applied automatically. Remaining transplants
are sourced from //graft-maven-plugin/configuration/paths.
TODO
The core tests cover most transplant use-cases; they show what is possible and how to accomplish it.
@Graft.Recipient(ClassNode.class)
public class ClassNodeTransplant {
    
    @Graft.Mock
    public String name;
    
    public String toString() {
        return "ClassNode(" + name + ")";
    }
}