graftt - annotation-driven bytecode surgery
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.
An agent (java -javaagent
) applies transplants at load-time. Alternatively, graftt-maven-plugin
finds and applies transplants within target/classes
.
The core
module can be used for building custom tools. It imposes few restrictions on usage and revolves around ASM's tree API.
graftt sprung from artemis-odb/570, refer to it for the discussion leading up to this.
See wiki for more documentation.
Use-cases
- Functionality for editors: callbacks and tighter tooling integration
- Additional debugging capabilities: logging, record stack traces, additional callbacks
- Extending existing functionality for final or otherwise non-extendable classes
- The odd bug fix in imported dependencies
- Add or modify
hashcode()
andtoString()
- Retrofit classes with additional interfaces
Example Transplant for SomeClass
A third-party class we wish to modify
public class SomeClass {
public final void yo() {
yolo();
}
private void yolo() {
// boo! we want to call "invokedWithTransplant = true"
// here (for some reason or other), but yo() is final
// and can't be extended, and this method is private
//
// ...
//
yoloCalled = true;
}
public boolean yoloCalled = false;
public static boolean invokedWithTransplant = false;
}
Create a transplant class to donate some bytecode
@Graft.Recipient(SomeClass.class) // target to modify
public class SomeClassTransplant {
@Graft.Fuse // fuse with method in SomeClass
private void yolo() { // signature matches SomeClass.yolo()
SomeClass.invokedWithTransplant = true; // whoop-whoop
yolo(); // "recursive continuation", actually invokes SomeClass::yolo
}
}
Resulting class
Once transplanted, decompiling the modified class yields something similar to:
public class SomeClass {
public final void yo() {
yolo();
}
private void yolo() {
SomeClass.invokedWithTransplant = true;
yolo$original();
}
private void yolo$original() {
yoloCalled = true;
}
public boolean yoloCalled = false;
public static boolean invokedWithTransplant = false;
}
API
@Graft.Recipient
specifies which class to transplant to.@Graft.Fuse
transplants bytecode over to@Graft.Recipient
, translating anyFooTransplant
references toFoo
. Call the original method at any time by invoking the method currently being fused; e.g. FusingFooTransplant::bar
withFoo::bar
, any call tobar()
inside the transplant will point toFoo::bar$original
once applied.@Graft.Mock
to 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.Annotations
overrides 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.
Nice to have, but not now:
@Graft.Remove
: Remove field or method from target class.
Caveats
- You're working against internal implementation; there are no semver guarantees
- No rewiring of parent type on target class
- No
@Graft.Fuse
for constructors; nice to have, but not initially - No GWT support
- No android support (possible with a custom gradle task)
Usage
Current $VERSION
is 0.3.0
Maven
<dependency>
<groupId>net.onedaybeard.graftt</groupId>
<artifactId>api</artifactId>
<version>${VERSION}</version>
</dependency>
Gradle
implementation "net.onedaybeard.graftt:api:${VERSION}"
Agent: Download
$ java -ea -javaagent:agent-${VERSION}.jar
...