Repository navigation
Home
Rewrite existing classes by grafting bytecode from Transplant classes. Transplants are plain java classes and function like templates or blueprints; annotations define interactions with the recipient class. The entire API consists of 4 primary 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
- Terminology
- API
-
How-to
- Add field to class
- Add method to class
- Fuse(/add) method in class
- Replace method in class
- Add annotations
- Update or remove annotation on class, method and field
- Add interface to class
- Transplant and recipient are interchangeable in transplants
- Mocking fields and methods
- Ad-hoc mocking external classes
- Limitations and caveats
- Usage
- Custom tools
- Recipes
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.Annotationsconfigures removal and updating of annotations. The default behavior copies all annotations from the transplant class and its symbol over to the recipient. - All interfaces are copied over to 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.
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<>(); // :(
}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. None
of Graft.* annotations are transplanted.
@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 transplanted are added to the recipient.
@Graft.Recipient(Foo.class)
class FooTransplant implements Bar { // Bar interface transplanted to Foo
@Override
public int hi() { return "hello"; }
}Note that only interfaces are supported; changing the parent class is not supported.
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);
}
}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();
}
}- Not yet possible to append to constructor or static initializer.
- Transplanted fields are initialized to
nullor0. - Parent class of recipient cannot be changed.
- Not possible to remove fields, methods or interfaces.
- No support for multiple transplants denoting the same recipient; will fail if both
try to
@Graft.Fusewith the same method.
Some of these shortcomings will be addressed in a later versions.
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:
- build-time maven plugin: transplanting during compilation. After compilation, resulting classes have been transformed by matching transplants.
- load-time java agent: transplanting before class reaches the classloader. Transformed classes exist only in memory and die when the application ends.
The maven plugin and agent differ in which classes are eligible for transplant. The maven
plugin is restricted to only transplanting to classes under target/classes, while
the agent has free reign over where recipient classes originate from.
The use-case for 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 agent works by transforming classes before they're loaded by the classloader.
There are two ways registering transplants:
- Transplants on the project classpath: transplants are registered by instantiation.
- Pass root directories and path/to/jar: transplants are registered automatically.
When no arguments are passed to the agent, each transplant has to be instantiated (loaded by the classloader) for it to be registered.
# all transplants live on classpath; registration through instantiation
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()
BarTransplant()
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 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>graftt</id>
<goals>
<goal>graftt</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 + ")";
}
}Any interfaces implemented by the transplant are carried over 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;
}
}If 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(); }
@Graft.Fuse @Override
public int y() { return y(); }
}