Zero‑Restart Hotfixes in Java: Using ByteBuddy for Runtime Class Modification

This article demonstrates how to apply ByteBuddy in Java 21 to create dynamic classes, delegate methods, define new methods and fields, and redefine existing classes at runtime, enabling urgent bug fixes and logic changes without restarting the application.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Zero‑Restart Hotfixes in Java: Using ByteBuddy for Runtime Class Modification

1. Introduction

In Java development, fixing urgent bugs or adjusting business logic without restarting the application is often difficult. Runtime class modification solves this problem, and ByteBuddy provides a powerful API to achieve it.

2. Environment

Java 21

3. Dependency

<dependency>
  <groupId>net.bytebuddy</groupId>
  <artifactId>byte-buddy</artifactId>
  <version>1.17.8</version>
</dependency>

4. Creating a Dynamic Class

Using ByteBuddy we subclass Object, intercept the toString method and return a fixed string.

DynamicType.Unloaded<Object> unloadedType = new ByteBuddy()
    .subclass(Object.class)
    .method(ElementMatchers.isToString())
    .intercept(FixedValue.value("Hello ByteBuddy!"))
    .make();
Class<?> dynamicType = unloadedType
    .load(RuntimeCreateClassTest.class.getClassLoader())
    .getLoaded();
String ret = dynamicType.getDeclaredConstructor().newInstance().toString();
System.err.println(ret);

Running the code prints Hello ByteBuddy!. The explanation notes that subclass() creates a new subclass of Object, ElementMatchers.isToString() selects the toString method, and intercept() supplies a custom implementation.

5. Method Delegation (Custom Logic)

Two simple classes are defined:

public class Person {
  public String say() { return "Person say"; }
}
public class Student {
  public static String say() { return "Student say"; }
}

ByteBuddy delegates Person.say() to Student.say():

Class<? extends Person> clazz = new ByteBuddy()
    .subclass(Person.class)
    .method(named("say").and(isDeclaredBy(Person.class)).and(returns(String.class)))
    .intercept(MethodDelegation.to(Student.class))
    .make()
    .load(MethodDelegationCustomLogicTest.class.getClassLoader())
    .getLoaded();
String ret = clazz.getDeclaredConstructor().newInstance().say();
System.err.println(ret);

The output is Student say. ByteBuddy matches the target method by name, declaring class and return type.

6. Selecting Between Multiple Candidates

If the target class contains several methods with the same signature, ByteBuddy can use the @BindingPriority annotation to decide.

public class Student {
  @BindingPriority(1)
  public static String say1() { /* ... */ }

  @BindingPriority(2)
  public static String speek() { /* ... */ }
}

Running the delegation now prints Student speek, showing that the method with the higher priority (2) is chosen.

7. Defining New Methods and Fields

ByteBuddy can also create a brand‑new class, add a method and a field, and intercept the method with another class.

Class<?> type = new ByteBuddy()
    .subclass(Object.class)
    .name("MyPackClass")
    .defineMethod("run", String.class, Modifier.PUBLIC)
    .intercept(MethodDelegation.to(Student.class))
    .defineField("id", String.class, Modifier.PUBLIC)
    .make()
    .load(DefinitionMethodAndFieldTest.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
    .getLoaded();
Method m = type.getDeclaredMethod("run");
Object instance = type.getDeclaredConstructor().newInstance();
Object ret = m.invoke(instance);
System.err.println(ret);
System.err.println(type.getDeclaredField("id").get(instance));

The method call prints Student speek and the newly defined field is null because it was never assigned.

8. Redefining an Existing Class

To modify a class that is already loaded, add the byte-buddy-agent dependency and install the agent.

<dependency>
  <groupId>net.bytebuddy</groupId>
  <artifactId>byte-buddy-agent</artifactId>
  <version>1.17.8</version>
</dependency>
ByteBuddyAgent.install();
new ByteBuddy()
    .redefine(Person.class)
    .method(named("say"))
    .intercept(FixedValue.value("Person Value Redefined"))
    .make()
    .load(Student.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
Person p = new Person();
String ret = p.say();
System.err.println(ret);

The result is Person Value Redefined. The agent installs a Java agent into the running JVM, and redefine() replaces the implementation of the selected method.

9. Conclusion

The examples illustrate how ByteBuddy can be used for zero‑restart hot‑fixes: creating new classes, delegating methods, adding members, and redefining existing classes, all without stopping the application.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaByteBuddyHotfixRuntime InstrumentationDynamic ClassClass RedefinitionMethod Delegation
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.