The JVM (Java Virtual Machine) is an excellent platform for monkey-patching.
Monkey patching is a technique used to dynamically update the behavior of a piece of code at run-time. A monkey patch (also spelled monkey-patch, MonkeyPatch) is a way to extend or modify the runtime code of dynamic languages (e.g. Smalltalk, JavaScript, Objective-C, Ruby, Perl, Python, Groovy, etc.) without altering the original source code.
-- Wikipedia
I want to demo several approaches for monkey-patching in Java in this post.
As an example, I'll use a sample for-loop. Imagine we have a class and a method. We want to call the method multiple times without doing it explicitly.
While the Decorator Design Pattern is not monkey-patching, it's an excellent introduction to it anyway. Decorator is a structural pattern described in the foundational book, Design Patterns: Elements of Reusable Object-Oriented Software.
The decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
Our use case is a Logger
interface with a dedicated console implementation:
We can implement it in Java like this:
public interface Logger {
void log(String message);
}
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println(message);
}
}
Here's a simple, configurable decorator implementation:
public class RepeatingDecorator implements Logger { //1
private final Logger logger; //2
private final int times; //3
public RepeatingDecorator(Logger logger, int times) {
this.logger = logger;
this.times = times;
}
@Override
public void log(String message) {
for (int i = 0; i < times; i++) { //4
logger.log(message);
}
}
}
Must implement the interface
Underlying logger
Loop configuration
Call the method as many times as necessary
Using the decorator is straightforward:
var logger = new ConsoleLogger();
var threeTimesLogger = new RepeatingDecorator(logger, 3);
threeTimesLogger.log("Hello world!");
The Java Proxy is a generic decorator that allows attaching dynamic behavior:
Proxy provides static methods for creating objects that act like instances of interfaces but allow for customized method invocation.
The Spring Framework uses Java Proxies a lot. It's the case of the @Transactional
annotation. If you annotate a method, Spring creates a Java Proxy around the encasing class at runtime. When you call it, Spring calls the proxy instead. Depending on the configuration, it opens the transaction or joins an existing one, then calls the actual method, and finally commits (or rollbacks).
The API is simple:
We can write the following handler:
public class RepeatingInvocationHandler implements InvocationHandler {
private final Logger logger; //1
private final int times; //2
public RepeatingInvocationHandler(Logger logger, int times) {
this.logger = logger;
this.times = times;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
if (method.getName().equals("log") && args.length ## 1 && args[0] instanceof String) { //3
for (int i = 0; i < times; i++) {
method.invoke(logger, args[0]); //4
}
}
return null;
}
}
Here's how to create the proxy:
var logger = new ConsoleLogger();
var proxy = (Logger) Proxy.newProxyInstance( //1-2
Main.class.getClassLoader(),
new Class[]{Logger.class}, //3
new RepeatingInvocationHandler(logger, 3)); //4
proxy.log("Hello world!");
Proxy
objectLogger
as the API was created before generics, and it returns an Object
Instrumentation is the capability of the JVM to transform bytecode before it loads it via a Java agent. Two Java agent flavors are available:
The Instrumentation API's surface is limited:
As seen above, the API exposes the user to low-level bytecode manipulation via byte arrays. It would be unwieldy to do it directly. Hence, real-life projects rely on bytecode manipulation libraries. ASM has been the traditional library for this, but it seems that Byte Buddy has superseded it. Note that Byte Buddy uses ASM but provides a higher-level abstraction.
The Byte Buddy API is outside the scope of this blog post, so let's dive directly into the code:
public class Repeater {
public static void premain(String arguments, Instrumentation instrumentation) { //1
var withRepeatAnnotation = isAnnotatedWith(named("ch.frankel.blog.instrumentation.Repeat")); //2
new AgentBuilder.Default() //3
.type(declaresMethod(withRepeatAnnotation)) //4
.transform((builder, typeDescription, classLoader, module, domain) -> builder //5
.method(withRepeatAnnotation) //6
.intercept( //7
SuperMethodCall.INSTANCE //8
.andThen(SuperMethodCall.INSTANCE)
.andThen(SuperMethodCall.INSTANCE))
).installOn(instrumentation); //3
}
}
main
method, with the added Instrumentation
argument@Repeat
annotation. The DSL reads fluently even if you don't know it (I don't).@Repeat
annotation@Repeat
The next step is to create the Java agent package. A Java agent is a regular JAR with specific manifest attributes. Let's configure Maven to build the agent:
<plugin>
<artifactId>maven-assembly-plugin</artifactId> <!--1-->
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef> <!--2-->
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>ch.frankel.blog.instrumentation.Repeater</Premain-Class> <!--3-->
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase> <!--4-->
</execution>
</executions>
</plugin>
Testing is more involved, as we need two different codebases, one for the agent and one for the regular code with the annotation. Let's create the agent first:
mvn install
We can then run the app with the agent:
java -javaagent:/Users/nico/.m2/repository/ch/frankel/blog/agent/1.0-SNAPSHOT/agent-1.0-SNAPSHOT-jar-with-dependencies.jar \ #1
-cp ./target/classes #2
ch.frankel.blog.instrumentation.Main #3
premain
method of the class configured in the agent
The idea behind AOP is to apply some code across different unrelated object hierarchies - cross-cutting concerns. It's a valuable technique in languages that don't allow traits, or code you can graft on third-party objects/classes. Fun fact: I learned about AOP before Proxy
. AOP relies on two main concepts: an aspect is the transformation applied to code, while a point cut matches where the aspect applies.
In Java, AOP's historical implementation is the excellent AspectJ library. AspectJ provides two approaches, known as weaving: build-time weaving, which transforms the compiled bytecode, and runtime weaving, which relies on the above instrumentation. Either way, AspectJ uses a specific format for aspects and pointcuts. Before Java 5, the format looked like Java but not quite; for example, it used the aspect
keyword. With Java 5, one can use annotations in regular Java code to achieve the same goal.
We need an AspectJ dependency:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.19</version>
</dependency>
As Byte Buddy, AspectJ also uses ASM underneath.
Here's the code:
@Aspect //1
public class RepeatingAspect {
@Pointcut("@annotation(repeat) && call(* *(..))") //2
public void callAt(Repeat repeat) {} //3
@Around("callAt(repeat)") //4
public Object around(ProceedingJoinPoint pjp, Repeat repeat) throws Throwable { //5
for (int i = 0; i < repeat.times(); i++) { //6
pjp.proceed(); //7
}
return null;
}
}
@Repeat
@Repeat
annotation to the the repeat
name used in the annotation above@Around
, meaning that we need to call the original method explicitlyProceedingJoinPoint
, which references the original method, as well as the @Repeat
annotation
At this point, we need to weave the aspect.
Let's do it at build-time. For this, we can add the AspectJ build plugin:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile</goal> <!--1-->
</goals>
</execution>
</executions>
</plugin>
Bind execution of the plugin to the compile
phase
To see the demo in effect:
mvnd compile exec:java -Dexec.mainClass=ch.frankel.blog.aop.Main
Last, it's possible to change the generated bytecode via a Java compiler plugin, introduced in Java 6 as JSR 269. From a bird's eye view, plugins involve hooking into the Java compiler to manipulate the AST in three phases: parse the source code into multiple ASTs, analyze further into Element
, and potentially generate source code.
The documentation could be less sparse. I found the following Awesome Java Annotation Processing. Here's a simplified class diagram to get you started:
I'm too lazy to implement the same as above with such a low-level API. As the expression goes, this is left as an exercise to the reader. If you are interested, I believe the DocLint
source code is a good starting point.
I described several approaches to monkey-patching in Java in this post: the Proxy
class, instrumentation via a Java Agent, AOP via AspectJ, and javac
compiler plugins. To choose one over the other, consider the following criteria: build-time vs. runtime, complexity, native vs. third-party, and security concerns.
To go further:
Originally published at A Java Geek on September 17th, 2023