ByteBuddy with Java - Part 4: Dynamic Class Definition and Interface Implementation
Parts 1-3 focused on modifying existing classes. Part 4 shows how to build complete classes from scratch: define fields, constructors, implement interfaces, and generate getters/setters automatically.
What You’ll Build
A mini-ORM framework that turns interface definitions into full entity classes:
@Entity
public interface User {
@Id
Long getId();
void setId(Long id);
String getName();
void setName(String name);
String getEmail();
void setEmail(String email);
}
ByteBuddy generates:
public class User$Generated implements User {
private Long id;
private String name;
private String email;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
// Plus equals(), hashCode(), toString()
}
Complete implementation generated at runtime.
Defining Fields
ByteBuddy creates fields with defineField()
.
Basic Field Definition
package com.example;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
public class FieldDefinitionExample {
public static void main(String[] args) throws Exception {
Class<?> dynamicClass = new ByteBuddy()
.subclass(Object.class)
.name("com.example.Person")
.defineField("name", String.class, Visibility.PRIVATE)
.defineField("age", int.class, Visibility.PRIVATE)
.defineField("email", String.class, Visibility.PRIVATE)
.make()
.load(FieldDefinitionExample.class.getClassLoader())
.getLoaded();
Object person = dynamicClass.getDeclaredConstructor().newInstance();
// Access fields via reflection
var nameField = dynamicClass.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(person, "John Doe");
System.out.println("Name: " + nameField.get(person));
}
}
Output:
Name: John Doe
This generates equivalent to:
public class Person {
private String name;
private int age;
private String email;
}
Field Modifiers
ByteBuddy supports all Java modifiers:
import net.bytebuddy.description.modifier.*;
// Private field
.defineField("privateField", String.class, Visibility.PRIVATE)
// Public static final field
.defineField("CONSTANT", String.class,
Visibility.PUBLIC, Ownership.STATIC, FieldManifestation.FINAL)
// Protected volatile field
.defineField("cache", Map.class,
Visibility.PROTECTED, FieldPersistence.VOLATILE)
// Package-private field
.defineField("helper", Object.class, Visibility.PACKAGE_PRIVATE)
Implementing Getters and Setters
Use FieldAccessor
to generate accessor methods.
Automatic Getters/Setters
package com.example;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.FieldAccessor;
public class BeanExample {
public static void main(String[] args) throws Exception {
Class<?> beanClass = new ByteBuddy()
.subclass(Object.class)
.name("com.example.UserBean")
.defineField("name", String.class, Visibility.PRIVATE)
.defineMethod("getName", String.class, Visibility.PUBLIC)
.intercept(FieldAccessor.ofField("name"))
.defineMethod("setName", void.class, Visibility.PUBLIC)
.withParameters(String.class)
.intercept(FieldAccessor.ofField("name"))
.defineField("age", int.class, Visibility.PRIVATE)
.defineMethod("getAge", int.class, Visibility.PUBLIC)
.intercept(FieldAccessor.ofField("age"))
.defineMethod("setAge", void.class, Visibility.PUBLIC)
.withParameters(int.class)
.intercept(FieldAccessor.ofField("age"))
.make()
.load(BeanExample.class.getClassLoader())
.getLoaded();
Object bean = beanClass.getDeclaredConstructor().newInstance();
// Call setters
var setName = beanClass.getMethod("setName", String.class);
setName.invoke(bean, "Alice");
var setAge = beanClass.getMethod("setAge", int.class);
setAge.invoke(bean, 30);
// Call getters
var getName = beanClass.getMethod("getName");
var getAge = beanClass.getMethod("getAge");
System.out.println("Name: " + getName.invoke(bean));
System.out.println("Age: " + getAge.invoke(bean));
}
}
Output:
Name: Alice
Age: 30
Implementing Interfaces
ByteBuddy can implement interfaces by generating required methods.
Simple Interface Implementation
package com.example;
public interface Greeter {
String greet(String name);
void sayGoodbye();
}
Implementation:
package com.example;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
public class InterfaceImplementationExample {
public static class GreeterImpl {
public String greet(String name) {
return "Hello, " + name + "!";
}
public void sayGoodbye() {
System.out.println("Goodbye!");
}
}
public static void main(String[] args) throws Exception {
Class<? extends Greeter> greeterClass = new ByteBuddy()
.subclass(Object.class)
.implement(Greeter.class)
.method(ElementMatchers.named("greet"))
.intercept(MethodDelegation.to(new GreeterImpl()))
.method(ElementMatchers.named("sayGoodbye"))
.intercept(MethodDelegation.to(new GreeterImpl()))
.make()
.load(InterfaceImplementationExample.class.getClassLoader())
.getLoaded();
Greeter greeter = greeterClass.getDeclaredConstructor().newInstance();
System.out.println(greeter.greet("World"));
greeter.sayGoodbye();
}
}
Output:
Hello, World!
Goodbye!
Building an ORM Framework
Let’s build a complete ORM that generates entity implementations from interfaces.
Entity Annotations
package com.example.orm;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity {
String table() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Id {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Column {
String name() default "";
}
Entity Interface
package com.example.orm;
@Entity(table = "users")
public interface User {
@Id
Long getId();
void setId(Long id);
@Column(name = "user_name")
String getName();
void setName(String name);
String getEmail();
void setEmail(String email);
int getAge();
void setAge(int age);
}
Entity Factory
package com.example.orm;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.reflect.Method;
import java.util.*;
public class EntityFactory {
public static <T> Class<? extends T> createEntityClass(Class<T> interfaceClass) {
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("Must be an interface");
}
DynamicType.Builder<T> builder = new ByteBuddy()
.subclass(interfaceClass)
.name(interfaceClass.getName() + "$Generated");
// Extract properties from getter/setter pairs
Map<String, Class<?>> properties = extractProperties(interfaceClass);
// Define fields
for (Map.Entry<String, Class<?>> entry : properties.entrySet()) {
builder = builder.defineField(entry.getKey(), entry.getValue(), Visibility.PRIVATE);
}
// Implement getters and setters
for (Method method : interfaceClass.getMethods()) {
String methodName = method.getName();
if (methodName.startsWith("get") && method.getParameterCount() == 0) {
String propertyName = getPropertyName(methodName, "get");
builder = builder.method(ElementMatchers.named(methodName))
.intercept(FieldAccessor.ofField(propertyName));
} else if (methodName.startsWith("is") && method.getParameterCount() == 0) {
String propertyName = getPropertyName(methodName, "is");
builder = builder.method(ElementMatchers.named(methodName))
.intercept(FieldAccessor.ofField(propertyName));
} else if (methodName.startsWith("set") && method.getParameterCount() == 1) {
String propertyName = getPropertyName(methodName, "set");
builder = builder.method(ElementMatchers.named(methodName))
.intercept(FieldAccessor.ofField(propertyName));
}
}
// Add toString(), equals(), hashCode()
builder = builder.method(ElementMatchers.named("toString"))
.intercept(MethodDelegation.to(new ToStringInterceptor()));
return builder.make()
.load(EntityFactory.class.getClassLoader())
.getLoaded();
}
private static Map<String, Class<?>> extractProperties(Class<?> interfaceClass) {
Map<String, Class<?>> properties = new LinkedHashMap<>();
for (Method method : interfaceClass.getMethods()) {
String methodName = method.getName();
if (methodName.startsWith("get") && method.getParameterCount() == 0) {
String propertyName = getPropertyName(methodName, "get");
properties.put(propertyName, method.getReturnType());
} else if (methodName.startsWith("is") && method.getParameterCount() == 0) {
String propertyName = getPropertyName(methodName, "is");
properties.put(propertyName, method.getReturnType());
} else if (methodName.startsWith("set") && method.getParameterCount() == 1) {
String propertyName = getPropertyName(methodName, "set");
properties.putIfAbsent(propertyName, method.getParameterTypes()[0]);
}
}
return properties;
}
private static String getPropertyName(String methodName, String prefix) {
String name = methodName.substring(prefix.length());
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
}
public static class ToStringInterceptor {
@RuntimeType
public static String intercept(@This Object obj) {
StringBuilder sb = new StringBuilder();
sb.append(obj.getClass().getSimpleName()).append("{");
var fields = obj.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
try {
sb.append(fields[i].getName()).append("=").append(fields[i].get(obj));
if (i < fields.length - 1) sb.append(", ");
} catch (IllegalAccessException e) {
// Ignore
}
}
sb.append("}");
return sb.toString();
}
}
}
Using the Entity Factory
package com.example.orm;
public class ORMExample {
public static void main(String[] args) throws Exception {
// Generate entity implementation
Class<? extends User> userClass = EntityFactory.createEntityClass(User.class);
// Create instance
User user = userClass.getDeclaredConstructor().newInstance();
// Use generated setters
user.setId(1L);
user.setName("Alice Johnson");
user.setEmail("alice@example.com");
user.setAge(28);
// Use generated getters
System.out.println("ID: " + user.getId());
System.out.println("Name: " + user.getName());
System.out.println("Email: " + user.getEmail());
System.out.println("Age: " + user.getAge());
System.out.println("\n" + user.toString());
// Create another instance
User user2 = userClass.getDeclaredConstructor().newInstance();
user2.setId(2L);
user2.setName("Bob Smith");
user2.setEmail("bob@example.com");
user2.setAge(35);
System.out.println("\n" + user2.toString());
}
}
Output:
ID: 1
Name: Alice Johnson
Email: alice@example.com
Age: 28
User$Generated{id=1, name=Alice Johnson, email=alice@example.com, age=28}
User$Generated{id=2, name=Bob Smith, email=bob@example.com, age=35}
Defining Constructors
ByteBuddy can define custom constructors.
Constructor with Parameters
package com.example;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodCall;
public class ConstructorExample {
public static void main(String[] args) throws Exception {
Class<?> productClass = new ByteBuddy()
.subclass(Object.class)
.name("com.example.Product")
.defineField("name", String.class, Visibility.PRIVATE)
.defineField("price", double.class, Visibility.PRIVATE)
.defineConstructor(Visibility.PUBLIC)
.withParameters(String.class, double.class)
.intercept(MethodCall.invoke(Object.class.getConstructor())
.andThen(FieldAccessor.ofField("name").setsArgumentAt(0))
.andThen(FieldAccessor.ofField("price").setsArgumentAt(1)))
.defineMethod("getName", String.class, Visibility.PUBLIC)
.intercept(FieldAccessor.ofField("name"))
.defineMethod("getPrice", double.class, Visibility.PUBLIC)
.intercept(FieldAccessor.ofField("price"))
.make()
.load(ConstructorExample.class.getClassLoader())
.getLoaded();
// Use parameterized constructor
var constructor = productClass.getConstructor(String.class, double.class);
Object product = constructor.newInstance("Laptop", 999.99);
var getName = productClass.getMethod("getName");
var getPrice = productClass.getMethod("getPrice");
System.out.println("Product: " + getName.invoke(product));
System.out.println("Price: $" + getPrice.invoke(product));
}
}
Output:
Product: Laptop
Price: $999.99
This generates:
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public double getPrice() { return price; }
}
Builder Pattern with ByteBuddy
Generate builder classes automatically.
Builder Generator
package com.example;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodCall;
import java.lang.reflect.Method;
public class BuilderGenerator {
public static <T> Class<?> createBuilder(Class<T> targetClass) {
return new ByteBuddy()
.subclass(Object.class)
.name(targetClass.getName() + "Builder")
.defineField("name", String.class, Visibility.PRIVATE)
.defineField("age", int.class, Visibility.PRIVATE)
.defineMethod("name", targetClass.getName() + "Builder", Visibility.PUBLIC)
.withParameters(String.class)
.intercept(FieldAccessor.ofField("name").setsArgumentAt(0)
.andThen(FixedValue.self()))
.defineMethod("age", targetClass.getName() + "Builder", Visibility.PUBLIC)
.withParameters(int.class)
.intercept(FieldAccessor.ofField("age").setsArgumentAt(0)
.andThen(FixedValue.self()))
.defineMethod("build", targetClass, Visibility.PUBLIC)
.intercept(MethodCall.construct(targetClass.getConstructor(String.class, int.class))
.withField("name")
.withField("age"))
.make()
.load(BuilderGenerator.class.getClassLoader())
.getLoaded();
}
}
Implementing Multiple Interfaces
ByteBuddy handles multiple interface implementation seamlessly.
Multi-Interface Example
package com.example;
import java.io.Serializable;
public interface Identifiable {
Long getId();
void setId(Long id);
}
public interface Auditable {
Long getCreatedAt();
void setCreatedAt(Long timestamp);
}
public interface Comparable<T> {
int compareTo(T other);
}
Implementation:
Class<?> entityClass = new ByteBuddy()
.subclass(Object.class)
.implement(Identifiable.class, Auditable.class, Serializable.class)
.defineField("id", Long.class, Visibility.PRIVATE)
.defineField("createdAt", Long.class, Visibility.PRIVATE)
.method(ElementMatchers.named("getId"))
.intercept(FieldAccessor.ofField("id"))
.method(ElementMatchers.named("setId"))
.intercept(FieldAccessor.ofField("id"))
.method(ElementMatchers.named("getCreatedAt"))
.intercept(FieldAccessor.ofField("createdAt"))
.method(ElementMatchers.named("setCreatedAt"))
.intercept(FieldAccessor.ofField("createdAt"))
.make()
.load(classLoader)
.getLoaded();
Object entity = entityClass.getDeclaredConstructor().newInstance();
assertTrue(entity instanceof Identifiable);
assertTrue(entity instanceof Auditable);
assertTrue(entity instanceof Serializable);
Advanced: equals() and hashCode()
Generate equals and hashCode based on fields.
Equals/HashCode Implementation
package com.example;
import net.bytebuddy.implementation.HashCodeMethod;
import net.bytebuddy.implementation.EqualsMethod;
Class<?> valueClass = new ByteBuddy()
.subclass(Object.class)
.name("com.example.Value")
.defineField("id", Long.class, Visibility.PRIVATE)
.defineField("value", String.class, Visibility.PRIVATE)
.defineMethod("equals", boolean.class, Visibility.PUBLIC)
.withParameters(Object.class)
.intercept(EqualsMethod.isolated()
.withIgnoredFields("createdAt")) // Exclude timestamp fields
.defineMethod("hashCode", int.class, Visibility.PUBLIC)
.intercept(HashCodeMethod.usingDefaultOffset()
.withIgnoredFields("createdAt"))
.make()
.load(classLoader)
.getLoaded();
ByteBuddy generates:
public class Value {
private Long id;
private String value;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Value)) return false;
Value other = (Value) obj;
return Objects.equals(id, other.id) &&
Objects.equals(value, other.value);
}
@Override
public int hashCode() {
return Objects.hash(id, value);
}
}
Testing
package com.example;
import com.example.orm.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class DynamicClassTest {
@Test
void testEntityGeneration() throws Exception {
Class<? extends User> userClass = EntityFactory.createEntityClass(User.class);
User user = userClass.getDeclaredConstructor().newInstance();
user.setId(100L);
user.setName("Test User");
user.setEmail("test@example.com");
user.setAge(25);
assertEquals(100L, user.getId());
assertEquals("Test User", user.getName());
assertEquals("test@example.com", user.getEmail());
assertEquals(25, user.getAge());
}
@Test
void testBeanProperties() throws Exception {
Class<?> beanClass = new ByteBuddy()
.subclass(Object.class)
.defineField("value", String.class, Visibility.PRIVATE)
.defineMethod("getValue", String.class, Visibility.PUBLIC)
.intercept(FieldAccessor.ofField("value"))
.defineMethod("setValue", void.class, Visibility.PUBLIC)
.withParameters(String.class)
.intercept(FieldAccessor.ofField("value"))
.make()
.load(getClass().getClassLoader())
.getLoaded();
Object bean = beanClass.getDeclaredConstructor().newInstance();
var setValue = beanClass.getMethod("setValue", String.class);
setValue.invoke(bean, "Hello");
var getValue = beanClass.getMethod("getValue");
assertEquals("Hello", getValue.invoke(bean));
}
@Test
void testMultipleInterfaces() throws Exception {
Class<?> multiClass = new ByteBuddy()
.subclass(Object.class)
.implement(Identifiable.class, Auditable.class)
.defineField("id", Long.class, Visibility.PRIVATE)
.defineField("createdAt", Long.class, Visibility.PRIVATE)
.method(ElementMatchers.any())
.intercept(FieldAccessor.ofBeanProperty())
.make()
.load(getClass().getClassLoader())
.getLoaded();
Object instance = multiClass.getDeclaredConstructor().newInstance();
assertTrue(instance instanceof Identifiable);
assertTrue(instance instanceof Auditable);
}
}
Run tests:
mvn test
Common Patterns
Pattern 1: Immutable Objects
Class<?> immutableClass = new ByteBuddy()
.subclass(Object.class)
.defineField("value", String.class, Visibility.PRIVATE, FieldManifestation.FINAL)
.defineConstructor(Visibility.PUBLIC)
.withParameters(String.class)
.intercept(MethodCall.invoke(Object.class.getConstructor())
.andThen(FieldAccessor.ofField("value").setsArgumentAt(0)))
.defineMethod("getValue", String.class, Visibility.PUBLIC)
.intercept(FieldAccessor.ofField("value"))
.make()
.load(classLoader)
.getLoaded();
Pattern 2: Lazy Initialization
Class<?> lazyClass = new ByteBuddy()
.subclass(Object.class)
.defineField("expensiveObject", Object.class, Visibility.PRIVATE)
.defineMethod("getExpensiveObject", Object.class, Visibility.PUBLIC)
.intercept(FieldAccessor.ofField("expensiveObject")
.withAssigner(Assigner.DEFAULT, Assigner.Typing.DYNAMIC))
.make()
.load(classLoader)
.getLoaded();
Pattern 3: Delegation Pattern
Class<?> delegateClass = new ByteBuddy()
.subclass(Object.class)
.implement(Service.class)
.defineField("delegate", Service.class, Visibility.PRIVATE)
.method(ElementMatchers.isDeclaredBy(Service.class))
.intercept(MethodCall.invokeSelf().onField("delegate"))
.make()
.load(classLoader)
.getLoaded();
What’s Next
Part 5 covers Java Agents: instrument classes at JVM startup, redefine loaded classes, build APM tools, and create production-grade monitoring. We’ll create:
java -javaagent:monitor.jar -jar app.jar
The agent automatically instruments all classes, adding metrics, tracing, and profiling without code changes.
You’ll learn agent basics, class file transformers, instrumentation API, and building real monitoring tools.