ByteBuddy with Java - Part 4: Dynamic Class Definition and Interface Implementation

Build complete classes with ByteBuddy: define fields, constructors, implement interfaces, and create a mini-ORM framework with automatic getters/setters.

October 7, 2025
40 minutes
By Prashant Chaturvedi
ByteBuddy Java Bytecode ORM Interfaces Class Generation

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.