Open In App

Monkey Patching in Java

Last Updated : 20 Mar, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Software development often involves optimizing and enhancing the existing functionality of our systems. Sometimes modifying an existing codebase may not be possible or the most practical solution. So, the solution to this problem is a Monkey Patching. This approach allows us to modify the runtime of a class or module without modifying the source code.

In this article, we will learn how to implement Monkey Patching in Java.

Monkey Patching

Monkey Patching is a method for dynamically changing a piece of code’s functionality during runtime. A Monkey Patch is a means to add to or change the dynamic languages’ runtime code (also called monkey-patch, MonkeyPatch).

Step-by-Step Implementation of Monkey Patching in Java

Below are the steps to implement Monkey Patching in Java.

Step 1: Create Dynamic Proxies

Let us start the process for our Wedding Planner interface with some logs using dynamic proxies. java.lang.reflect.InvocationHandler must first be subtyped as follows:

Java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class WeddingPlanner implements InvocationHandler 
{
    private final Object target;

    public WeddingPlanner(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object args[]) throws Throwable 
    {
        // log before the method invocation
        System.out.println("Before method: " + method.getName());

        // invoke method on the original object
        Object result = method.invoke(target, args);

        // log after method invocation
        System.out.println("After method: " + method.getName());

        // return the result of method invocation
        return result;
    }
}


Step 2: Construct a test for dynamic proxy

To find out if logs, let’s write a test:

Java
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Proxy;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class WeddingPlannerTest 
{

    @Test
    public void whenMethodCalled_thenSurroundedByLogs() {
        // redirect System.out to capture logs
        ByteArrayOutputStream logOutputStream = new ByteArrayOutputStream();
        System.setOut(new PrintStream(logOutputStream));

        // create an instance of WeddingPlanner
        WeddingPlanner weddingPlanner = new WeddingPlanner(new WeddingPlannerImpl());

        // create a proxy for the WeddingPlanner using a WeddingPlanner
        WeddingPlanner proxy = (WeddingPlanner) Proxy.newProxyInstance(
                WeddingPlanner.class.getClassLoader(),
                new Class[]{WeddingPlanner.class},
                new WeddingPlanner(weddingPlanner)
        );

        // call the planWedding method on the proxy
        proxy.planWedding("Ankita", "Subhashree");

        // retrieve logs from the logOutputStream
        String logOutput = logOutputStream.toString();

        // assert that the logs contain the expected messages
        assertTrue(logOutput.contains("Before method: planWedding"));
        assertTrue(logOutput.contains("After method: planWedding"));
    }
}


Step 3: Implement the Planner Pattern

We are going to construct a new class that will implement the WeddingPlanner interface in order to implement this pattern. The request will be processed by a property of type WeddingPlanner. Additionally, the only things our planner does are relay the request for the planning of the wedding and add a few logs.

Java
public class WeddingPlannerImpl implements WeddingPlanner {
    @Override
    public void planWedding(String groomName, String brideName) {
        // Business logic for planning a wedding
        System.out.println("Wedding planned for " + groomName + " and " + brideName);
    }
}


Step 4: Use Aspect-Oriented Programming

Two methods that AspectJ offers are called weaving: runtime weaving and build-time weaving, which modify the produced bytecode.

Java
@Aspect
public class RepeatingAspect {

    // Pointcut definition using annotation and method call expression
    @Pointcut("@annotation(repeat) && call(* *(..))")
    public void callAt(Repeat repeat) {}

    // advice method that runs around the specified pointcut
    @Around("callAt(repeat)")
    public Object around(ProceedingJoinPoint pp, Repeat repeat) throws Throwable 
    {
        // execute method specified number of times
        for (int i = 0; i < repeat.times(); i++) 
        {
            pp.proceed();
        }
        return null; // returning null as it seems this advice doesn't modify the original method's result
    }
}


Step 6: Create a Reflection

Assume for the moment that the plan has been changed to a different idea. Because it is hardcoded and we didn’t define setters for our planWedding class, we are unable to modify it. But we can change the venue and cost to the new number and break encapsulation using reflection.

Java
import java.lang.reflect.Field;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class WeddingPlannerReflectionTest 
{

    @Test
    public void givenPrivateField_whenUsingReflection_thenBehaviorCanBeChanged() throws IllegalAccessException, NoSuchFieldException {
        // create an instance of WeddingPlannerImpl
        WeddingPlannerImpl weddingPlanner = new WeddingPlannerImpl();

        // get the private field "venueCost" from WeddingPlannerImpl
        Field venueCost = WeddingPlannerImpl.class.getDeclaredField("venueCost");

        // allow access to private field
        venueCost.setAccessible(true);

        // modify the value of the private field to 15000
        venueCost.set(weddingPlanner, 15000);

        // call the planWedding method on the modified object
        double result = weddingPlanner.calculateTotalCost(10000);

        // assert that the result is as expected after modifying the private field
        assertEquals(25000, result);
    }
}
  • The capacity of a program to analyze and alter its behavior during runtime is known as reflection.
  • The Reflections library or the java.lang.reflect package in Java allow us to utilize it. It should be used cautiously since it may have an influence on the performance and maintainability of the code.
  • Accessing class information, examining fields and methods, and even calling methods at runtime are popular uses of reflection for monkey patching.
  • As such, this feature makes it possible to make changes at runtime without having to change the source code itself.


Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads