DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World
  • The Energy Efficiency of JVMs and the Role of GraalVM
  • Understanding Root Causes of Out of Memory (OOM) Issues in Java Containers

Trending

  • Can You Run a MariaDB Cluster on a $150 Kubernetes Lab? I Gave It a Shot
  • The Role of AI in Identity and Access Management for Organizations
  • Scaling Microservices With Docker and Kubernetes on Production
  • The End of “Good Enough Agile”
  1. DZone
  2. Coding
  3. Java
  4. Monkey-Patching in Java

Monkey-Patching in Java

Learn about 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.

By 
Nicolas Fränkel user avatar
Nicolas Fränkel
DZone Core CORE ·
Sep. 21, 23 · Analysis
Likes (6)
Comment
Save
Tweet
Share
6.1K Views

Join the DZone community and get the full member experience.

Join For Free

The JVM 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.

The Decorator Design Pattern

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.

— Decorator pattern

Our use-case is a Logger interface with a dedicated console implementation:

We can implement it in Java like this:

Java
 
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:

Java
 
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);
        }
    }
}


  1. Must implement the interface
  2. Underlying logger
  3. Loop configuration
  4. Call the method as many times as necessary

Using the decorator is straightforward:

Java
 
var logger = new ConsoleLogger();
var threeTimesLogger = new RepeatingDecorator(logger, 3);
threeTimesLogger.log("Hello world!");


The Java Proxy

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.

— Proxy Javadoc

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:

API

We can write the following handler:

Java
 
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;
    }
}


  1. Underlying logger
  2. Loop configuration
  3. Check every requirement is upheld
  4. Call the initial method on the underlying logger

Here's how to create the proxy:

Java
 
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!");


  1. Create the Proxy object
  2. We must cast to Logger as the API was created before generics, and it returns an Object
  3. Array of interfaces the object needs to conform to
  4. Pass our handler

Instrumentation

Instrumentation is the capability of the JVM to transform bytecode before it loads it via a Java agent. Two Java agent flavors are available:

  • Static, with the agent passed on the command line when you launch the application
  • Dynamic allows connecting to a running JVM and attaching an agent on it via the Attach API. Note that it represents a huge security issue and has been drastically limited in the latest JDK.

The Instrumentation API's surface is limited:

java.lang.instrument

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:

Java
 
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
  }
}


  1. Required signature; it's similar to the main method, with the added Instrumentation argument
  2. Match that is annotated with the @Repeat annotation. The DSL reads fluently even if you don't know it (I don't).
  3. Byte Buddy provides a builder to create the Java agent
  4. Match all types that declare a method with the @Repeat annotation
  5. Transform the class accordingly
  6. Transform methods annotated with @Repeat
  7. Replace the original implementation with the following
  8. Call the original implementation three times

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:

XML
 
<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>


  1. Create a JAR containing all dependencies ()

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:

Shell
 
mvn install


We can then run the app with the agent:

Shell
 
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


  1. Run Java with the agent created in the previous step. The JVM will run the premain method of the class configured in the agent
  2. Configure the classpath
  3. Set the main class

Aspect-Oriented Programming

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, 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:

XML
 
<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:

Java
 
@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;
    }
}


  1. Mark this class as an aspect
  2. Define the pointcut; every call to a method annotated with @Repeat
  3. Bind the @Repeat annotation to the the repeat name used in the annotation above
  4. Define the aspect applied to the call site; it's an @Around, meaning that we need to call the original method explicitly
  5. The signature uses a ProceedingJoinPoint, which references the original method, as well as the @Repeat annotation
  6. Loop over as many times as configured
  7. Call the original method

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:

XML
 
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>                  <!--1-->
            </goals>
        </execution>
    </executions>
</plugin>


  1. Bind execution of the plugin to the compile phase

To see the demo in effect:

Shell
 
mvn compile exec:java -Dexec.mainClass=ch.frankel.blog.aop.Main


Java Compiler Plugin

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:

Java Compiler Plugin

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.

Conclusion

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

  • Monkey patch
  • Guide to Java Instrumentation
  • Byte Buddy
  • Creating a Java Compiler Plugin
  • Awesome Java Annotation Processing
  • Maven AspectJ plugin
Java virtual machine Monkey patch Java (programming language)

Published at DZone with permission of Nicolas Fränkel, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World
  • The Energy Efficiency of JVMs and the Role of GraalVM
  • Understanding Root Causes of Out of Memory (OOM) Issues in Java Containers

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!