How Byte Buddy Powers Java Agents: Classloader Tricks and Dependency Solutions

This article explores using Byte Buddy to build Java agents, detailing the premain method, class loading mechanisms, handling dependency conflicts with Maven shade versus custom classloaders, and implementing a Dispatcher to bridge agent and application classloaders, providing practical code snippets and diagrams for each step.

Tech Musings
Tech Musings
Tech Musings
How Byte Buddy Powers Java Agents: Classloader Tricks and Dependency Solutions

Introduction

Byte Buddy is a code generation and manipulation library that allows creating and modifying Java classes at runtime without a compiler. The author revisits Byte Buddy documentation and a Bilibili tutorial by "Yuan Wei" to share practical experiences of building Java agents.

Byte Buddy is a code generation and manipulation library for creating and modifying Java classes during the runtime of a Java application and without the help of a compiler.

1. Java Agent Loading

When a Java application starts, the JVM loads classes through a series of steps: verification, transformation, and loading. A Java agent can intercept this process, similar to AOP, to apply custom transformations.

Java file loading mechanism
Java file loading mechanism

The premain method provided by Byte Buddy is invoked before the application's main method. Its signature is:

public static void premain(String args, Instrumentation inst)

During execution of a simple HelloWorld program, the JVM loads HelloWorld.class, the agent’s premain intercepts it, applies the transformation defined in the agent, and the transformed class ( helloWorld.class) is then loaded by the ClassLoader.

Agent premain invocation
Agent premain invocation

2. Dependency Conflicts

2.1 Conventional Solution

In real projects, an agent may bring many dependencies that clash with the application's own libraries. If the conflicting dependency is API‑compatible, you can exclude it in pom.xml:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-core</artifactId>
  <version>${spring.version}</version>
  <exclusions>
    <exclusion>
      <artifactId>commons-logging</artifactId>
      <groupId>commons-logging</groupId>
    </exclusion>
  </exclusions>
</dependency>

If the APIs differ, simple exclusions are insufficient. The maven-shade-plugin can relocate packages to avoid clashes, but this approach has drawbacks in large projects, such as missed transitive dependencies, runtime surprises, and debugging difficulty.

Complex dependency graphs in big projects.

Easy to overlook a transitive dependency, causing runtime failures.

Renamed packages hinder debugging.

Therefore, the agent should avoid using maven-shade-plugin for conflict resolution and instead adopt a custom ClassLoader strategy.

2.2 Custom ClassLoader

Java class loading follows the parent‑delegation model: Application ClassLoader → Extension ClassLoader → Bootstrap ClassLoader. EaseAgent implements a custom ClassLoader whose parent is the Bootstrap ClassLoader, isolating agent dependencies from the application’s own classes.

Agent and Application dependency isolation
Agent and Application dependency isolation

3. ClassLoader Dependency Transfer

To enhance a user method printiInfo, the agent injects advice code. However, the advice references ObjectUtils.isEmpty(obj), which resides in a library loaded by the agent’s custom ClassLoader. When the application’s ClassLoader tries to load this advice, it cannot find ObjectUtils, resulting in a ClassNotFoundException.

Class missing exception
Class missing exception

The solution is to create a Dispatcher that lives in the Bootstrap ClassLoader. The Dispatcher holds a map of Action implementations (provided by the agent). When the application’s ClassLoader loads the advice, it delegates the lookup of the Action to the Dispatcher, which already has access to the required dependencies.

// Byte Buddy API to register a class into the Bootstrap ClassLoader
ClassInjector.UsingInstrumentation
  .of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, instrumentation)
  .inject(Collections.singletonMap(describe.resolve(),
          classFileLocator.locate(className).resolve()));

The Dispatcher is essentially a Map<String, Action> where each Action encapsulates the code to be executed. The agent registers its Actions during startup, and the application retrieves and runs them via the Dispatcher, eliminating the missing‑dependency problem.

Dispatcher implementation
Dispatcher implementation

Summary

Java agents built with Byte Buddy offer powerful, non‑intrusive instrumentation, making them attractive for APM and monitoring solutions. However, developing agents is challenging due to limited documentation, the need for deep Java knowledge, and the fast pace of JDK releases (EaseAgent currently supports only Java 8). Proper handling of classloader isolation and dependency transfer—using custom ClassLoaders and a Bootstrap‑level Dispatcher—mitigates conflicts and ensures reliable agent behavior.

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.

InstrumentationmavenclassloaderJava AgentByte Buddy
Tech Musings
Written by

Tech Musings

Capturing thoughts and reflections while coding.

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.