PolarSPARC |
Introduction to Byte Buddy
Bhaskar S | 05/12/2024 |
Overview
Have you ever pondered on what is the one common magical ingredient behind two of the most popular Java frameworks - Hibernate AND Mockito ???
In the case of Hibernate, once a data class is annotated as an Entity, one is able to perform database CRUD operations using instances of that data class. Similarly, with Mockito, one is able to "mock" the functionality of any class.
That common "magical" ingredient between the two frameworks is - Byte Buddy !!!
Byte Buddy is a popular Java code generation and manipulation library that can create and/or modify Java classes at runtime.
Of course, there are the other popular Java code generation and manipulation libraries such as, ASM and Javassist. These other frameworks require a little more intimate knowledge of the JVM instruction set in order to use them effectively.
For those interested, had published an article on Bytecode Handling with ASM a while back.
What Byte Buddy brings to the table is a higher level, user-friendly "fluent " style APIs, which do not require any prior knowledge of the JVM instruction set.
Setup
The setup will be on a Ubuntu 22.04 LTS based Linux desktop. Ensure at least Java 11 or above is installed and setup. Also, ensure Apache Maven is installed and setup.
To setup the Java directory structure for the demonstrations in this article, execute the following commands:
$ cd $HOME
$ mkdir -p $HOME/java/ByteBuddy
$ cd $HOME/java/ByteBuddy
$ mkdir -p src/main/java src/main/resources
$ mkdir -p src/main/java/com/polarsparc/bytebuddy
The following is the listing for the Maven project file pom.xml that will be used:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.polarsparc.bytebuddy</groupId> <artifactId>ByteBuddy</artifactId> <version>1.0</version> <properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> <slf4j.version>2.0.13</slf4j.version> <mockito.version>5.11.0</mockito.version> <bytebuddy.version>1.14.15</bytebuddy.version> </properties> <dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> </dependency> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>${bytebuddy.version}</version> </dependency> </dependencies> </project>
The following is the listing for the slf4j-simple logger properties file simplelogger.properties located in the directory src/main/resources:
# ### SLF4J Simple Logger properties # org.slf4j.simpleLogger.defaultLogLevel=info org.slf4j.simpleLogger.showDateTime=true org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS org.slf4j.simpleLogger.showThreadName=true
Hands-on with Byte Buddy
The following is the Java code for a simple message class:
/* * Name: SimpleMessage * Author: Bhaskar S * Date: 05/11/2024 * Blog: https://www.polarsparc.com */ package com.polarsparc.bytebuddy; import java.util.concurrent.TimeUnit; public class SimpleMessage { public String getMessage() { return "Hello from getMessage !!!"; } public String getMessageAfterDelay(long delay) throws InterruptedException { TimeUnit.MILLISECONDS.sleep(delay); return "Hello from getMessageAfterDelay !!!"; } }
In order to demonstrate what the "magical" ingredient implies, let us consider the following Java code using Mockito:
/* * Name: SimpleMock * Author: Bhaskar S * Date: 05/11/2024 * Blog: https://www.polarsparc.com */ package com.polarsparc.bytebuddy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.mockito.Mockito; public class SimpleMock { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleMock.class); public static void main(String[] args) { SimpleMessage message = Mockito.mock(SimpleMessage.class); Mockito.when(message.getMessage()).thenReturn("Hello from Mockito !!!"); LOGGER.info(message.getMessage()); } }
The call Mockito.mock(SimpleMessage.class) creates a "mock" version of the given class.
Next, the call Mockito.when(message.getMessage()).thenReturn("Hello from Mockito !!!") uses the "mock" version of the class to return the string value Hello from Mockito !!! when the method getMessage() is invoked.
Under the hood the Mockito library uses Byte Buddy code to intercept the call to the getMessage() to return the desired string value.
To demonstrate a simple "HelloWorld" style example, let us consider the following Java code that uses Byte Buddy to dynamically create a Java class at run-time:
/* * Name: Sample_1 * Author: Bhaskar S * Date: 05/11/2024 * Blog: https://www.polarsparc.com */ package com.polarsparc.bytebuddy; import java.lang.reflect.InvocationTargetException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.bytebuddy.ByteBuddy; import net.bytebuddy.implementation.FixedValue; import net.bytebuddy.matcher.ElementMatchers; public class Sample_1 { private static final Logger LOGGER = LoggerFactory.getLogger(Sample_1.class); public static void main(String[] args) { try { LOGGER.info(fixedValueToString()); } catch (Exception e) { throw new RuntimeException(e); } } public static Class<?> generateDynamicClass() { return new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.isToString()) .intercept(FixedValue.value("Hello from PolarSPARC !!!")) .make() .load(Sample_1.class.getClassLoader()) .getLoaded(); } public static String fixedValueToString() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { Class<?> clazz = generateDynamicClass(); return clazz.getDeclaredConstructor().newInstance().toString(); } }
The instance of the class net.bytebuddy.ByteBuddy is the main entry point for performing any form of code generation or manipulation using Byte Buddy.
The method subclass(Class<T> type) returns a builder that subclasses using the specified Java class.
The class ElementMatchers is a utility class that provides various methods for matching code elements such as fields, methods, etc.
The method isToString() on the utility class matches the toString() method of the Java Object.class.
The method FixedValue.value(value) allows one to return a desired fixed value.
The method intercept() redefines the matched code element with the specified implementation.
The method make() creates the new class type from the configured builder.
The method load(ClassLoader loader) loads the create class into the Java class loader.
The method getLoaded() returns the loaded Java class to the caller.
To execute the code from Listing.3, open a terminal window and run the following commands:
$ cd $HOME/java/ByteBuddy
$ mvn exec:java -Dexec.mainClass="com.polarsparc.bytebuddy.Sample_1"
The following would be the typical output:
[INFO] Scanning for projects... [INFO] [INFO] -----------------< com.polarsparc.bytebuddy:ByteBuddy >----------------- [INFO] Building ByteBuddy 1.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- exec:3.2.0:java (default-cli) @ ByteBuddy --- 2024-05-12 07:27:43:777 [com.polarsparc.bytebuddy.Sample_1.main()] INFO com.polarsparc.bytebuddy.Sample_1 - Hello from PolarSPARC !!! [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.382 s [INFO] Finished at: 2024-05-12T07:27:43-04:00 [INFO] ------------------------------------------------------------------------
Next, to demonstrate how one could mimic the behavior of Mockito using Byte Buddy to intercept the call to the method getMessage() from the Java class SimpleMessage, let us consider the following Java code which dynamically modifies the given Java class at run-time:
/* * Name: Sample_2 * Author: Bhaskar S * Date: 05/11/2024 * Blog: https://www.polarsparc.com */ package com.polarsparc.bytebuddy; import java.lang.reflect.InvocationTargetException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.bytebuddy.ByteBuddy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatchers; public class Sample_2 { private static final Logger LOGGER = LoggerFactory.getLogger(Sample_2.class); public static void main(String[] args) { try { LOGGER.info(interceptorForToString()); } catch (Exception e) { throw new RuntimeException(e); } } public static Class<? extends SimpleMessage> generateDynamicInterceptedClass() { return new ByteBuddy() .subclass(SimpleMessage.class) .method( ElementMatchers.named("getMessage") .and(ElementMatchers.isDeclaredBy(SimpleMessage.class)) .and(ElementMatchers.returns(String.class)) ) .intercept(MethodDelegation.to(Sample_2.SimpleInterceptor.class)) .make() .load(Sample_3.class.getClassLoader()) .getLoaded(); } public static String interceptorForToString() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { Class<? extends SimpleMessage> clazz = generateDynamicInterceptedClass(); return clazz.getDeclaredConstructor().newInstance().getMessage(); } public static class SimpleInterceptor { public static String interceptor() { return "Hola from SimpleInterceptor !!!"; } } }
The method ElementMatchers.returns(class) tries to match the specify method element with the specific return type.
The method ElementMatchers.isDeclaredBy(class) tries to match the specify named type element in the specified class.
The method ElementMatchers.named(name) tries to match the specify named code element from the specific subclass.
The method MethodDelegation.to(class) tries to delegate the specified method call to another method in a different class. Note that one must provide reference to the delegated class if the delegated method is static OR provide an instance of the delegated class if delegated method is an instance method.
To execute the code from Listing.4, open a terminal window and run the following commands:
$ cd $HOME/java/ByteBuddy
$ mvn exec:java -Dexec.mainClass="com.polarsparc.bytebuddy.Sample_2"
The following would be the interaction and the corresponding output:
[INFO] Scanning for projects... [INFO] [INFO] -----------------< com.polarsparc.bytebuddy:ByteBuddy >----------------- [INFO] Building ByteBuddy 1.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- exec:3.2.0:java (default-cli) @ ByteBuddy --- 2024-05-12 07:29:55:350 [com.polarsparc.bytebuddy.Sample_2.main()] INFO com.polarsparc.bytebuddy.Sample_2 - Hola from SimpleInterceptor !!! [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.373 s [INFO] Finished at: 2024-05-12T07:29:55-04:00 [INFO] ------------------------------------------------------------------------
BAM !!! We have successfully demonstrated the behavior of Mockito.
There are situations when one would like to determine the execution time of some of the method(s) in a Java class. One can easily achieve this capability using Byte Buddy method interceptor.
The following Java code demonstrates how one could determine the execution time of a specific method at run-time using a method interceptor:
/* * Name: Sample_3 * Author: Bhaskar S * Date: 05/11/2024 * Blog: https://www.polarsparc.com */ package com.polarsparc.bytebuddy; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.bytebuddy.ByteBuddy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatchers; import net.bytebuddy.implementation.bind.annotation.*; public class Sample_3 { private static final Logger LOGGER = LoggerFactory.getLogger(Sample_3.class); public static void main(String[] args) { try { LOGGER.info(interceptorForToString()); } catch (Exception e) { throw new RuntimeException(e); } } public static Class<? extends SimpleMessage> generateDynamicInterceptedClass() { return new ByteBuddy() .subclass(SimpleMessage.class) .method(ElementMatchers.hasMethodName("getMessageAfterDelay")) .intercept(MethodDelegation.to(Sample_3.TimingInterceptor.class)) .make() .load(Sample_3.class.getClassLoader()) .getLoaded(); } public static String interceptorForToString() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InterruptedException { Class<? extends SimpleMessage> clazz = generateDynamicInterceptedClass(); return clazz.getDeclaredConstructor().newInstance().getMessageAfterDelay(500); } public static class TimingInterceptor { @RuntimeType public static String interceptor(@This Object self, @Origin Method method, @AllArguments Object[] allArguments, @SuperMethod Method superMethod) throws Exception { long start = System.currentTimeMillis(); try { superMethod.invoke(self, allArguments); } catch (Exception e) { throw new RuntimeException(e); } finally { long elapsed = System.currentTimeMillis() - start; LOGGER.info("{} took {} ms", method.getName(), elapsed); } return "Hola from TimingInterceptor !!!"; } } }
The annotation RuntimeType will attempt to cast the return type to the method's return type.
The annotation This maps the object reference to the specific instrumented source object at run-time.
The annotation Origin provides metadata information about the instrumented source method at run-time.
The annotation SuperMethod maps the method instance to the instrumented source super method at run-time.
To execute the code from Listing.5, open a terminal window and run the following commands:
$ cd $HOME/java/ByteBuddy
$ mvn mvn exec:java -Dexec.mainClass="com.polarsparc.bytebuddy.Sample_3"
The following would be the typical output:
[INFO] -----------------< com.polarsparc.bytebuddy:ByteBuddy >----------------- [INFO] Building ByteBuddy 1.0 [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- exec:3.2.0:java (default-cli) @ ByteBuddy --- 2024-05-12 08:00:20:601 [com.polarsparc.bytebuddy.Sample_3.main()] INFO com.polarsparc.bytebuddy.Sample_3 - getMessageAfterDelay took 501 ms 2024-05-12 08:00:20:602 [com.polarsparc.bytebuddy.Sample_3.main()] INFO com.polarsparc.bytebuddy.Sample_3 - Hola from TimingInterceptor !!! [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.898 s [INFO] Finished at: 2024-05-12T08:00:20-04:00 [INFO] ------------------------------------------------------------------------
BINGO !!! We have successfully demonstrated how one could intercept method(s) to determine their execution time.
Note that we have barely scratched the surface of this very powerful swiss-army knife framework !!!
References