Byte Buddy is a library to help you create and modify Java classes and provides a feature for generating Java Agents.
This library is written in Java 5 but is compatible with any Java version. It’s also very lightweight and only depends on ASM.
Libraries like Mockito or Hibernate use Byte Buddy under the hood.
Getting Started
Installation
You can start using Byte Buddy in your own Java projects by including a single dependency:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.11.6</version>
</dependency>
How to Use
Method Overriding
You can use Byte Buddy by creating a instance of ByteBuddy
. i.e.:
@Test
void fixedValue() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.sergiomartinrubio.bytebuddyexample.NewClass")
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello Fixed Value!"))
.make()
.load(getClass().getClassLoader())
.getLoaded();
assertEquals("Hello Fixed Value!", dynamicType.getDeclaredConstructor().newInstance().toString());
}
The previous code will do the following:
- Define a class that inherits from
Object
. - Name the class with a fully qualified name
"com.sergiomartinrubio.bytebuddyexample.NewClass"
. - Override
toString
method fromObject
. - Replace the returned value of
toString
withHello Fixed Value!
. Did you noticed? This is similar to what Mockito does when you are mocking the return value of a particular method. make
triggers the generation of the class.- The class is loaded into the JVM with
load(getClass().getClassLoader())
. getLoaded
is used to return an instance of the new loaded class.
Instead of hardcoding the returned value in the class creation, we can use something called MethodDelegation
which allows you to pass an interceptor class which replace the method implementation with the “best method definition match”.
@Test
void methodDelegation() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<? extends Foo> dynamicType = new ByteBuddy()
.subclass(Foo.class)
.method(ElementMatchers.named("myFooMethod")
.and(ElementMatchers.isDeclaredBy(Foo.class))
.and(ElementMatchers.returns(String.class))
)
.intercept(MethodDelegation.to(FooInterceptor.class))
.make()
.load(getClass().getClassLoader())
.getLoaded();
assertEquals("Hello From Foo Interceptor!", dynamicType.getDeclaredConstructor().newInstance().myFooMethod());
}
given the parent class:
public class Foo {
public String myFooMethod() {
return "Hello from my Foo method!";
}
}
and interceptor class:
public class FooInterceptor {
public static String intercept() {
return "Hello From Foo Interceptor!";
}
}
In case you have multiple methods defined in the interceptor class with the same method arguments and return type we will get an exception like java.lang.IllegalArgumentException: Cannot resolve ambiguous delegation...
. We need to set priorities in this case:
public class FooInterceptor {
@BindingPriority(1)
public static String intercept() {
return "Hello From Foo Interceptor!";
}
@BindingPriority(2)
public static String secondInterceptor() {
return "Hello from second Interceptor";
}
}
The higher the number, the higher is the priority, therefore, the second method will be used.
Method Creation
We can also create methods from scratch as follows:
@Test
void newMethod() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.sergiomartinrubio.bytebuddyexample.NewClassWithNewMethod")
.defineMethod("invokeMyMethod", String.class, Modifier.PUBLIC)
.intercept(FixedValue.value("Hello from new method!"))
.make()
.load(getClass().getClassLoader())
.getLoaded();
Method method = dynamicType.getMethod("invokeMyMethod");
assertEquals(
"Hello From another interceptor!",
method.invoke(dynamicType.getDeclaredConstructor().newInstance())
);
}
- Define a class that inherits from
Object
. - Give a name to the class.
- Define public method
invokeMyMethod
with aString
return type. - Override return value for new method.
Field Overriding
You can also override the value of a constant (static
and final
fields) of an existing class:
@Test
void overrideFieldValue() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
Class<? extends Foo> dynamicType = new ByteBuddy()
.redefine(Foo.class)
.name("com.sergiomartinrubio.bytebuddyexample.NewFooClass")
.field(ElementMatchers.named("myField"))
.value("World")
.make()
.load(getClass().getClassLoader())
.getLoaded();
Field field = dynamicType.getDeclaredField("myField");
assertEquals(String.class, field.getGenericType());
assertEquals("World", field.get(dynamicType.getDeclaredConstructor().newInstance()));
}
- Make a copy of an existing class with
redefine
. - Use a new name of the copy.
- Select the field to override.
- Change value.
Field Creation
Similarly we can define fields with values as follows:
@Test
void newField() throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.sergiomartinrubio.bytebuddyexample.NewClassWithNewField")
.defineField("newField", String.class, Modifier.PUBLIC | Modifier.FINAL | Modifier.STATIC)
.value("Hello From New Field!")
.make()
.load(getClass().getClassLoader())
.getLoaded();
Field field = dynamicType.getDeclaredField("newField");
assertEquals(String.class, field.getGenericType());
assertEquals("Hello From New Field!", field.get(dynamicType.getDeclaredConstructor().newInstance()));
}
We have to define the field as STATIC
and FINAL
to be able to set the value.
If you want to set the value of an instance field you can do it by defining a constructor:
@Test
void newNonStaticField() throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.sergiomartinrubio.bytebuddyexample.NewClassWithNewMethod")
.defineConstructor(Modifier.PUBLIC)
.withParameters(String.class)
.intercept(MethodCall
.invoke(Object.class.getConstructor())
.andThen(FieldAccessor
.ofField("newField")
.setsArgumentAt(0)
)
)
.defineField("newField", String.class, Modifier.PUBLIC)
.make()
.load(getClass().getClassLoader())
.getLoaded();
Object newInstance = dynamicType.getConstructor(String.class)
.newInstance("Hello From New non Static Field!");
Field field = dynamicType.getDeclaredField("newField");
field.setAccessible(true);
assertEquals("Hello From New non Static Field!", field.get(newInstance));
}
Java Agents
Byte Buddy also provides an API for creating Java agents with new AgentBuilder()
. Therefore we can perform byte code manipulation at runtime.
This API has similar features to what AOP (Aspec Oriented Programming) libraries like AspectJ provides. Some of the features provided by the Byte Buddy Java Agent API are:
- Intercept the method execution and perform additional logic. You can use annotations like
@Advice.OnMethodEnter
,@Advice.OnMethodExit
,@Advice.Origin
or@Advice.Enter
. - Get method fields:
@Advice.AllArguments
- Add fields and methods to classes:
@Advice.FieldValue
In the following example we are going to show how to use the Agent builder with an agent that will intercept the method execution before and after the method is called.
The Main
class contains the target method invokeCustomMethod
.
public class Main {
public static void main(String[] args) {
System.out.println("Hello from main method!");
invokeCustomMethod();
}
public static void invokeCustomMethod() {
System.out.println("Hello from custom method!");
}
}
Remember to create the
MANIFEST.MF
file underresources/META-INF/
with something like:Manifest-Version: 1.0 Main-Class: com.sergiomartinrubio.bytebuddyclient.Main
In a separate project we can create our Java Agent with ByteBuddy. First we can define the “advice” class that will intercept the calls before and after a method is executed:
public class HelloAdvice {
@Advice.OnMethodEnter
static long invokeBeforeEnterMethod(
@Advice.Origin String method) {
System.out.println("Method invoked before enter method by: " + method);
return System.currentTimeMillis();
}
@Advice.OnMethodExit
static void invokeAfterExitMethod(
@Advice.Origin String method,
@Advice.Enter long startTime
) {
System.out.println("Method " + method + " took " + (System.currentTimeMillis() - startTime) + "ms");
}
}
The method annotated with @Advice.OnMethodEnter
will be executed before the intercepted method. @Advice.Origin
gives you information about the intercepted method. On the other hand, @Advice.OnMethodExit
will be executed after the intercepted method. @Advice.Enter
contains the value returned by the method annotated with @Advice.OnMethodEnter
so we can do things like the method execution time.
Finally we can define our “agent”:
class Agent {
public static void premain(String arguments, Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(ElementMatchers.any())
.transform((builder, typeDescription, classLoader, module) -> builder
.method(ElementMatchers.nameContainsIgnoreCase("custom"))
.intercept(Advice.to(HelloAdvice.class)))
.installOn(instrumentation);
}
}
- All classes are targeted:
.type(ElementMatchers.any())
. - Methods which name contain
custom
are targeted:.method(ElementMatchers.nameContainsIgnoreCase("custom"))
. - Select advice class:
.intercept(Advice.to(HelloAdvice.class)))
. - Creates and install the agent builder into a given
Instrumentation
.
As part of the Java Agent
.jar
you need to specify the location of thepremain
method. You can use the maven pluginmaven-shade-plugin
.<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <manifestEntries> <Premain-Class>com.sergiomartinrubio.adviceagent.Agent</Premain-Class> </manifestEntries> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build>
Byte Buddy offers many other features that are not covered on this article, like intercepting methods marked by a particular annotation, more granular element matchers for classes and methods, constructor interceptors…
Conclusion
You might never need to use this kind of libraries but in case you are planning to do some Java Class manipulation at runtime or create Java Agents, Byte Buddy is an excellent choice since it provides a very easy to use API.
Photo by Ambitious Creative Co. - Rick Barrett on Unsplash