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
toStringmethod fromObject. - Replace the returned value of
toStringwithHello Fixed Value!. Did you noticed? This is similar to what Mockito does when you are mocking the return value of a particular method. maketriggers the generation of the class.- The class is loaded into the JVM with
load(getClass().getClassLoader()). getLoadedis 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
invokeMyMethodwith aStringreturn 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.Originor@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.MFfile 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
customare 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
.jaryou need to specify the location of thepremainmethod. 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
