Why runtime code generation?

The Java language comes with a comparatively strict type system. Java requires all variables and objects to be of a specific type and any attempt to assign incompatible types always causes an error. These errors are usually emitted by the Java compiler or at the very least by the Java runtime when casting a type illegally. Such strict typing is often desirable, for example when writing business applications. Business domains can usually be described in such an explicit manner where any domain item represents its own type. This way, we can use Java to build very readable and robust applications where mistakes are caught close to their source. Among other things, it is Java's type system that is responsible for Java's popularity in enterprise programming.

However, by enforcing its strict type system, Java imposes limitations that restrict the language's scope in other domains. For example, when writing a general-purpose library that is to be used by other Java applications, we are normally not able to reference any type that is defined in the user's application because these types are unknown to us when our library is compiled. In order to call methods or to access fields of the user's unknown code, the Java Class Library comes with a reflection API. Using the reflection API, we are able to introspect unknown types and to call methods or access fields. Unfortunately, the use of the reflection API has two significant downsides:

  • Using the reflection API is slower than a hard-coded method invocation: First, one needs to perform a rather expensive method lookup to get hold of an object that describes a specific method. Up to JDK 17, the JVM invokes a method dynamically in two different ways: (i) by running native code which requires long run time compared to a direct invocation; (ii) or using a concept called inflation where the JNI-based method invocation is replaced by generated byte code that is injected into a dynamically created class (even the JVM itself uses code generation!). However, this deprecated Java's inflation system remains with the drawback of generating very general code that, for example, only works with boxed primitive types such that the performance drawback is not entirely settled. Despite JDK 18 provides a single new way to invoke a method dynamically using method handlers, performance is still an issue.
  • The reflection API defeats type-safety: Even though the JVM is capable of invoking code by reflection, the reflection API is itself not type-safe. When writing a library, this is not a problem as long as we do not need to expose the reflection API to the library's user. After all, we do not know the user code during compilation and could not validate our library code against its types. Sometimes, it is however required to expose the reflection API to a user by for example letting a library invoke one of our own methods for us. This is where using the reflection API becomes problematic, as the Java compiler would need to have all the information to validate our program's type safety. For example, when implementing a library for method-level security, a user of this library would want the library to invoke a method only after enforcing a security constraint. For this, the library would need to reflectively call a method after the user handed over the required arguments for this method. Doing so, there is however no longer a compile-time type check if these method arguments match with the method's reflective invocation. The method call is still validated but the check is delayed until runtime. Doing so, we voided a great feature of the Java programming language.

This is where runtime code generation can help us out. It allows us to emulate some features that are normally only accessible when programming in a dynamic languages without discarding Java's static type checks. This way, we can get the best of both worlds and additionally improve runtime performance. To get a better understanding of this problem, let us look at the example of implementing the mentioned method-level security library.

Writing a security library

Business applications can grow large and sometimes it is difficult to keep an overview of call stacks in our application. This can become problematic when we have crucial methods in our application that should only be called under specific conditions. Imagine a business application that implements a reset functionality that allows deleting everything from the application's database.

class Service {
  void deleteEverything() {
    // delete everything ...
  }
}

Such a reset should of course only be performed by administrators and never by a normal user of our application. By analyzing our source code, we could of course make sure that this will never happen. However, we can expect our application to grow and to be changed in the future. Therefore, we want to implement a tighter security model where the method invocation is guarded by an explicit check for the application's current user. We typically use a security framework to ensure that only an administrator can call the method.

For this purpose, assume that we are using a security framework with a public API as the following:

@Retention(RetentionPolicy.RUNTIME)
@interface Secured {
  String user();
}

class UserHolder {
  static String user;
}

interface Framework {
  <T> T secure(Class<T> type);
}

In this framework, the Secured annotation should be used to mark methods that can only be accessed by a given user. The UserHolder is used for globally defining which user is currently logged into the application. The Framework interface allows for the creation of secured instances by calling the default constructor of a given type. Of course, this framework is overly simple, but in principle this is how security frameworks like for example the popular Spring Security work. A feature of this security framework is that we preserve the user's types. By the contract of our framework interface, we promise the user to return an instance of any type T it receives. Thanks to this, a user is able to interact with his own types as if the security framework did not exist. In a test environment, a user could even create unsecured instances of his types and use these instances instead of the secured ones. You will agree that this is really handy! Such frameworks are known to interact with POJOs, plain old Java objects, a term that was coined for describing non-intrusive frameworks that do not impose their own types upon their users.

Imagine for now that we knew that the type handed to the Framework could only be T = Service and that the deleteEverything method was annotated with @Secured("ADMIN"). This way, we could easily implement a secured version of this particular type by simply subclassing it:

class SecuredService extends Service {
  @Override
  void deleteEverything() {
    if(UserHolder.user.equals("ADMIN")) {
      super.deleteEverything();
    } else {
      throw new IllegalStateException("Not authorized");
    }
  }
}

With this additional class we could implement the framework as follows:

class HardcodedFrameworkImpl implements Framework {
  @Override
  public <T> T secure(Class<T> type) {
    if(type == Service.class) {
      return (T) new SecuredService();
    } else {
      throw new IllegalArgumentException("Unknown: " + type);
    }
  }
}

Of course, this implementation is not of much use. By the secure method's signature we suggested that the method can provide security for any type but in reality, we will throw an exception once we encounter something else then the known Service. Also, this would require our security library to know about this particular Service type when the library is compiled. Obviously, this is not a feasible solution for implementing the framework. So how can we solve this problem? Well, since this is a tutorial on a code generation library you will have guessed the answer: We create a subclass on demand and at runtime when the Service class first becomes known to our security framework by the invocation of the secure method. With code generation, we can take any given type, subclass it at runtime and override the methods we want to secure. In our case, we override all methods that are annotated with @Secured and read the required user from the annotation's user property. Many popular Java frameworks are implemented using a similar approach.

General information

Before we learn all about code generation and Byte Buddy, note that you should use code generation with care. Java types are something rather special to the JVM and are often not garbage collected. Therefore, you should never overuse code generation but only solve problems using generated code when it is the only way out. However, if you need to enhance unknown types as in the previous example, code generation is most likely your only option. Frameworks for security, transaction management, object-relational mapping or mocking are typical users of code generation libraries.

Of course, Byte Buddy is not the first library for code generation on the JVM. However, we believe that Byte Buddy knows some tricks the other frameworks cannot apply. The overall objective of Byte Buddy is to work declaratively, both by focusing on its domain specific language and the use of annotations. No other code generation library for the JVM we know of works this way. Nevertheless, you might want to have a look at some other frameworks for code generation to find out which one suites you best. Among others, the following libraries are prevalent in the Java space:

Java proxies
The Java Class Library (JCL) comes with a proxy toolkit that allows for the creation of classes that implement a given set of interfaces. This built-in proxy supplier is handy but also very limited. The above mentioned security framework could for example not be implemented this way since we want to extend classes and not interfaces.
cglib
The code generation library was implemented during the early years of Java and it did unfortunately not keep up with the development of the Java platform. Nevertheless, cglib remains a quite powerful library but its active development became rather vague. For this reason, many of its users moved away from cglib.
Javassist
This library comes with a compiler that takes strings containing Java source code which are translated into Java byte code during the runtime of an application. This is very ambitious and in principle a great idea since Java source code is obviously a great way for describing Java classes. However, the Javassist compiler does not compare to the javac compiler in its functionality and allows for easy mistakes when dynamically composing strings to implement more complex logic. Additionally, Javassist comes with a proxy library which is similar to the JCL's proxy utilities but allows extending classes and is not limited to interfaces. The scope of Javassist's proxy tools remain however equally limited in its API and functionality.

Evaluate the frameworks for yourself but we believe that Byte Buddy offers functionality and convenience that you will otherwise search in vain. Byte Buddy comes with an expressive domain specific language that allows for the creation of very custom runtime classes by writing plain Java code and by using strong typing for your own code. At the same time, Byte Buddy is very open for customization and does not restrain you to the features that come out of the box. If required, you can even define custom byte code for any implemented method. But even without knowing what byte code is or how it works, you are able to do quite a lot without digging deep into the framework. Did you for example have a look at the Hello World! example? Using Byte Buddy is that easy.

Of course, a pleasant API is not the only feature to consider when choosing a code generation library. For many applications, the runtime characteristics of generated code is more likely to determine a best choice. And beyond the runtime of the generated code itself, the runtime for creating a dynamic class can also be a concern. Claiming that We are the fastest! is as easy as it is hard to provide a valid metric for a library's speed. Still, we want to provide some metrics as a basic orientation. However, keep in mind that these results do not necessarily translate to your more specific use case where you should rather implement your own metrics.

Before discussing our metrics, let us look at the raw data. The following table displays an operation's average runtime in nanoseconds where the standard deviation is attached in braces:

Metric baseline Byte Buddy cglib Javassist Java proxy
trivial class creation (1) 0.003 (0.001) 142.772 (1.390) 515.174 (26.753) 193.733 (4.430) 70.712 (0.645)
interface implementation (2a) 0.004 (0.001) 1'126.364 (10.328) 960.527 (11.788) 1'070.766 (59.865) 1'060.766 (12.231)
stub method invocation (2b) 0.002 (0.001) 0.002 (0.001) 0.003 (0.001) 0.011 (0.001) 0.008 (0.001)
class extension (3a) 0.004 (0.001) 885.983
5'408.329
(7.901)
(52.437)
1'632.730 (52.737) 683.478 (6.735)
super method invocation (3b) 0.004 (0.001) 0.004
0.004
(0.001)
(0.001)
0.021 (0.001) 0.025 (0.001)

Similarly to static compilers, code generation libraries face a trade-off between generating fast code and generating code fast. When choosing between these conflicting goals, Byte Buddy's primary focus lies on generating code with minimal runtime. Typically, type creation or manipulation is not a common step within any program and does not significantly impact any long-running application; especially since class loading or class instrumentation is the most time-consuming and unavoidable step when running such code.

The first benchmark in the above table measures a library's runtime for subclassing Object without implementing or overriding any methods. This gives us an impression of a library's general overhead in code generation. In this benchmark, Java proxies perform better than other libraries due to optimizations that are only possible when assuming to always extend an interface. Byte Buddy also checks classes for generic types and annotations, which causes additional runtimes. This performance overhead is also visible in the other benchmarks for creating a class. Benchmark (2a) shows the measured runtime for creating (and loading) a class that implements a single interface with 18 methods, (2b) shows the execution time for the methods generated for this class. Similarly, (3a) shows a benchmark for extending a class with the same 18 methods which are implemented.

Byte Buddy provides two benchmarks, due to an optimization that is possible for an interceptor that always executes the super method. Sacrificing some time during class creation, the execution time of a Byte Buddy-created classes typically reaches the baseline, meaning that the instrumentation creates no overhead at all. It should be noted that Byte Buddy outperforms any other code generation library also during class creation, if the metadata processing was disabled. As the runtime of code generation is however so minimal compared to a program's total runtime, such an opt-out is not available as it would gain very little performance to the sacrifice of complicating the library code.

Finally, note that our metrics measure the performance of Java code that was priorly optimized by a JVM's just in time compiler. If your code is only executed occasionally, the performance will be worse than it is suggested by the above metrics. In this case, your code is however not performance-critical to begin with. The code for these metrics is distributed together with Byte Buddy and you can run these metrics on your own computer where the above numbers might be scaled depending on your machine's processing power. For this reason, do not interpret the above numbers absolutely, but consider them as a relative measure comparing different libraries. When further developing Byte Buddy, we want to monitor these metrics in order to avoid performance penalties when adding new features.

In the following tutorial we will gradually explain the features of Byte Buddy. We will start with its more general features which are most likely used by a majority of users. We will then consider increasingly advanced topics and give a short introduction to Java byte code and the class file format. And don't be discouraged in case you fast forward to this later material! You can do almost anything by using Byte Buddy's standard API and without understanding any JVM specifics. For learning about the standard API, just read on.

Creating a class

Any type that is created by Byte Buddy is emitted by an instance of the ByteBuddy class. Simply create a new instance by calling new ByteBuddy() and you are ready to go. Hopefully, you are using an development environment where you get suggestions on the methods that you can call on a given object. This way, you can avoid to manually look up a class's API in Byte Buddy's javadoc but have your IDE guide you through the process. As mentioned before, Byte Buddy offers a domain specific language which intends to be as human-readable as possible. Your IDE's hints will therefore point you into the right direction most of the time. But enough of the talking, let us create a first class at a Java program's runtime:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();

As it is hopefully obvious, the above code example creates a new class that extends the Object type. This dynamically created type would be equivalent to a Java class that only extends Object without explicitly implementing any methods, fields or constructors. You might have noted that we did not even name the dynamically generated type, something that normally is required when defining a Java class. Of course, you could have easily named your type explicitly:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make();

But what happens without the explicit naming? Byte Buddy lives and breaths of convention over configuration and provides you with defaults that we found convenient. As for the name of a type, the default Byte Buddy configuration provides a NamingStrategy which randomly creates a class name based on a dynamic type's superclass name. Furthermore, the name is defined to be in the same package as the super class such that package-private methods of the direct superclass are always visible to the dynamic type. If you for example subclassed a type named example.Foo, the generated name will be something like example.Foo$$ByteBuddy$$1376491271 where the numeric sequence is random. An exception of this rule is made when subclassing types from the java.lang package where types such as Object live. Java's security model does not allow custom types to live in this namespace. Therefore, such type names are prefixed with net.bytebuddy.renamed by the default naming strategy.

This default behavior might not be convenient for you. And thanks to the convention over configuration principle, you can always alter the default behavior by your needs. This is where the ByteBuddy class comes into place. By creating a new ByteBuddy() instance, you create a default configuration. By calling methods on this configuration, you can customize it by your individual needs. Let's try this:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .with(new NamingStrategy.AbstractBase() {
    @Override
    protected String name(TypeDescription superClass) {
        return "i.love.ByteBuddy." + superClass.getSimpleName();
    }
  })
  .subclass(Object.class)
  .make();

In the above code example, we created a new configuration that differs from the default configuration in its type naming strategy. The anonymous class is implemented to simply concatenate the string i.love.ByteBuddy and the base class's simple name. When subclassing the Object type, the dynamic type is therefore named i.love.ByteBuddy.Object. Be however careful when creating your own naming strategies! The Java virtual machine uses names to distinguish between types which is why you want to avoid naming collisions. If you need to customize the naming behavior, consider using Byte Buddy's built-in NamingStrategy.SuffixingRandom which you can customize to include a prefix that is more meaningful to your application than our default.

Domain specific language and immutability

After seeing Byte Buddy's domain specific language in action, we need to have a short look at the way this language is implemented. The one detail you need to know about the implementation is that the language is built around immutable objects. As a matter of fact, almost every class that lives in the Byte Buddy namespace was made immutable and in the few cases we could not make a type immutable, we explicitly mention it in this class's javadoc. If you implement custom features for Byte Buddy, we recommend you to stick with this principle.

As an implication of the mentioned immutability, you must be careful when, for example, configuring ByteBuddy instances. You must avoid mistakes such as the following one:

ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.with(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType = byteBuddy.subclass(Object.class).make();

You might expect the dynamic type to be generated using the custom naming strategy new NamingStrategy.SuffixingRandom("suffix") that was (allegedly) defined. Instead of mutating the instance that is stored in the byteBuddy variable, the invocation of the with method returns a customized ByteBuddy instance which is however lost. As a result, the dynamic type is created using the default configuration which was originally created.

Redefining and rebasing existing classes

So far, we only demonstrated how Byte Buddy can be used to create a subclass of an existing class. The same API can however be used for enhancing existing classes. Such enhancement is available in two different flavours:

type redefinition
When redefining a class, Byte Buddy allows for the alteration of an existing class, either by adding fields and methods or by replacing existing method implementations. Preexisting method implementations are however lost if they are replaced by another implementation. For example, when redefining the following type

class Foo {
  String bar() { return "bar"; }
}
to return "qux" from the bar method, the information that this method originally returned "bar" would be lost entirely.
type rebasing
When rebasing a class, Byte Buddy retains any method implementations of the rebased class. Instead of discarding overridden methods like when performing a type redefinition, Byte Buddy copies all such method implementations into renamed private methods with compatible signatures. This way, no implementation is lost and rebased methods can continue to invoke original code by calling these renamed methods. This way, the above class Foo could be rebased to something like

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}
where the information that the bar method originally returned "bar" is preserved within another method and therefore remains accessible. When rebasing a class, Byte Buddy treats all method definitions such as if you defined a subclass, i.e. it will call the rebased method if you attempt to call a rebased method's super method implementation. But instead, it eventually flattens this hypothetical super class into the rebased type displayed above.

Any rebasing, redefinition or subclassing is performed using an identical API which is defined by the DynamicType.Builder interface. This way, it is possible, for example, to define a class as a subclass and to later alter the definition to represent a rebased class instead. This is achieved by merely changing a single word of Byte Buddy's domain specific language. Applying either of the possible approaches

new ByteBuddy().subclass(Foo.class)
new ByteBuddy().redefine(Foo.class)
new ByteBuddy().rebase(Foo.class)

is handled transparently during further stages of the definition process, which is explained throughout the remainder of this tutorial. Because a subclass definition is a familiar concept to Java developers, all of the following explanations and examples of Byte Buddy's domain specific language are demonstrated by creating subclasses. However, keep in mind that all classes could similarly be defined by redefinition or by rebasing.

Loading a class

So far we only have defined and created a dynamic type but we did not make any use of it. A type that is created by Byte Buddy is represented by an instance of DynamicType.Unloaded. As the name suggests, these types are not loaded into the Java virtual machine. Instead, classes created by Byte Buddy are represented in their binary form, in the Java class file format. This way, it is up to you to decide what you want to do with a generated type. For example, you might want to run Byte Buddy from a build script that only generates classes to enhance a Java application before it is deployed. For this purpose, the DynamicType.Unloaded class allows to extract a byte array that represents the dynamic type. For convenience, the type additionally offers a saveIn(File) method that allows you to store a class in a given folder. Furthermore, it allows you to inject(File) classes into an existing jar file.

While directly accessing a class's binary form is straight forward, loading a type is unfortunately more complex. In Java, all classes are loaded using a ClassLoader. One example for such a class loader is the bootstrap class loader which is responsible for loading the classes that are shipped within the Java class library. The system class loader, on the other hand, is responsible for loading classes on the Java application's class path. Obviously, none of these preexisting class loaders is aware of any dynamic class we have created. To overcome this, we have to find other possibilities for loading a runtime generated class. Byte Buddy offers solutions by different approaches out of the box:

  • We simply create a new ClassLoader which is explicitly told about the existence of a particular dynamically created class. Because Java class loaders are organized in hierarchies, we define this class loader as the child of a given class loader that already exists in the running Java application. This way, all types of the running Java program are visible to the dynamic type that was loaded with new ClassLoader.
  • Normally, Java class loaders query their parent ClassLoader before attempting to directly load a type of a given name. This implies that a class loader normally never loads a type in case that its parent class loader is aware of a type with identical name. For this purpose, Byte Buddy offers the creation of a child-first class loader which attempts to load a type by itself before querying its parent. Other than that, this approach is similar to the approach just mentioned above. Note that this approach does not override a type of a parent class loader but rather shadows this other type.
  • Finally, we can use reflection to inject a type into an existent ClassLoader. Usually, a class loader is asked to provide a given type by its name. Using reflection, we can turn this principle around and call a protected method to inject a new class into the class loader without the class loader actually knowing how to locate this dynamic class.

Unfortunately, the above approaches have both their downsides:

  • If we create a new ClassLoader, this class loader defines a new namespace. As an implication, it is possible to load two classes with identical name as long as these classes are loaded by two different class loaders. These two classes are then never be considered as equal by a Java virtual machine, even if both classes represent an identical class implementation. This rule for equality holds however also for Java packages. This means that a class example.Foo is not able to access package-private methods of another class example.Bar if both classes were not loaded with the same class loader. Also, if example.Bar extended example.Foo, any overridden package-private methods would become inoperative, but still delegate to the original implementations.
  • Whenever a class is loaded, its class loader will look up any type that is referenced in this class once a code segment referencing another type is resolved. This lookup delegates to the same class loader. Imagine a scenario where we dynamically created two classes example.Foo and example.Bar. If we injected example.Foo into an existent class loader, this class loader might attempt to locate example.Bar. This lookup would however fail since the latter class was created dynamically and is unreachable for the class loader into which we just injected the example.Foo class. Therefore, the reflective approach cannot be used for classes with circular dependencies that become effective during class loading. Fortunately, most JVM implementations resolve referenced classes lazily on their first active use which is why class injection normally works without these restrictions. Also, in practice, classes that are created by Byte Buddy normally do not suffer from such circularity.

You might consider the chance of encountering circular dependencies to be of minor relevance since you are creating one dynamic type at a time. However, the dynamic creation of a type might trigger the creation of so-called auxiliary types. These types are created by Byte Buddy automatically to provide access to the dynamic type you are creating. We learn more about auxiliary types in the following section, do not worry about them for now. However, because of this, we recommend you to load dynamically created classes by creating a specific ClassLoader instead of injecting them into an existing one, whenever possible.

After creating a DynamicType.Unloaded, this type can be loaded using a ClassLoadingStrategy. If no such strategy is provided, Byte Buddy infers such a strategy based on the provided class loader. Then it creates a new class loader only for the bootstrap class loader, where no type can be injected using reflection, which is otherwise the default. Byte Buddy provides several class loading strategies out of the box, where each one follows the concepts described above. These strategies are defined in ClassLoadingStrategy.Default where the WRAPPER strategy creates a new, wrapping ClassLoader, where the CHILD_FIRST strategy creates a similar class loader with child-first semantics and where the INJECTION strategy injects a dynamic type using reflection. Both the WRAPPER and the CHILD_FIRST strategies are also available in so-called manifest versions where a type's binary format is preserved even after a class was loaded. These alternative versions make the binary representation of a class loader's classes accessible via the ClassLoader::getResourceAsStream method. However, note that this requires these class loaders to maintain a reference to the full binary representation of a class, what consumes space on a JVM's heap. Therefore, you should only use the manifest versions if you plan to actually access the binary format. Since the INJECTION strategy works via reflection and without a possibility to change the semantics of the ClassLoader::getResourceAsStream method, it is naturally not available in a manifest version.

Let's look at such class loading in action:

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

In the above example, we have created and loaded a class. We used the WRAPPER strategy for loading the class which is suitable for most cases, as we mentioned it before. Finally, the getLoaded method returns an instance of a Java Class that represents the dynamic class which is now loaded.

Note that when loading classes, the predefined class loading strategies are executed by applying the ProtectionDomain of the current execution context. Alternatively, all default strategies offer the specification of an explicit protection domain by calling the with method. Defining an explicit protection domain is important when using security managers (disabled in JDK 24 and marked for removal) or when working with classes that are defined in signed jars.

Reloading a class

In a previous section, we learned how Byte Buddy can be used in order to redefine or to rebase an existing class. During the execution of a Java program, it is however often impossible to guarantee that a specific class is not already loaded. Thanks to the Java virtual machine's HotSwap feature, existing classes can however be redefined even after they are loaded. This feature is made accessible by Byte Buddy's ClassReloadingStrategy. Let us demonstrate this strategy by redefining a class Foo:

class Foo {
  String m() { return "foo"; }
}

class Bar {
  String m() { return "bar"; }
}

Using Byte Buddy, we can now easily redefine the class Foo to become Bar. The code below renames the class Bar to Foo. After the redefinition, when we reference the latter, the former is referenced instead. Using HotSwap, this redefinition will even apply for preexisting instances:

// Requires byte-buddy-agent dependency
ByteBuddyAgent.install();

Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
assertThat(foo.m(), is("bar"));

HotSwap is only accessible using a so-called Java agent. Such an agent can be installed by either specifying it on the startup of the Java virtual machine by using the -javaagent parameter where the parameter's argument needs to be Byte Buddy's agent jar which can be downloaded from Maven Central. However, when a Java application is run from a JDK-installation of the Java virtual machine, Byte Buddy can load a Java agent even after application startup by ByteBuddyAgent.install(). Because class redefinition is mostly used to implement tooling or testing, this can be a very convenient alternative. Since Java 9, an agent installation is also possible at runtime without a JDK-installation.

One thing that might first appear counter-intuitive about the above example is the fact that Byte Buddy is instructed to redefine the Bar type where the Foo type is eventually redefined. The Java virtual machine identifies types by their name and a class loader. Therefore, by renaming Bar to Foo and applying this definition, we eventually redefine the type we renamed Bar into. It is of course equally possible to redefine Foo directly without renaming a different type.

Using Java's HotSwap feature, there is however one huge drawback. Current implementations of HotSwap require that the redefined classes apply the same class schema both before and after a class redefinition. This means that it is not allowed to add methods or fields when reloading classes. We already discussed that Byte Buddy defines copies of the original methods for any rebased class such that class rebasing does not work for the ClassReloadingStrategy. Also, class redefinition does not work for classes with an explicit class initializer method (a static block within a class) because this initializer needs to be copied into an extra method as well. Unfortunately OpenJDK has withdrawn from extending HotSwap functionality, so there is no way to work around this limitation using the HotSwap feature. In the mean time, Byte Buddy's HotSwap support can be used for corner-cases where it seems useful. Otherwise, class rebasing and redefinition can be a convenient feature when enhancing existing classes from for example a build script.

Working with unloaded classes

With this realization about the limits of Java's HotSwap feature, one might think that the only meaningful application of the rebase and redefinition instructions would be during build time. By applying build-time manipulation, one can assert that a processed class is not loaded before its initial class loading, simply because this class loading is accomplished in a different instance of the JVM. Byte Buddy is however equally capable of working with classes that were not yet loaded. For this, Byte Buddy abstracts over Java's reflection API such that a Class instance is for example internally represented by an instance of a TypeDescription. As a matter of fact, Byte Buddy only knows how to process a provided Class by an adapter that implements the TypeDescription interface. The big advantage over this abstraction is that information on classes do not need to be provided by a ClassLoader but can be provided by any other sources.

Byte Buddy provides a canonical manner for getting hold of a class's TypeDescription using a TypePool. A default implementation of such a pool is of course also provided. This TypePool.Default implementation parses the binary format of a class and represents it as the required TypeDescription. Similarly to a ClassLoader, it maintains a cache for represented classes which is also customizable. Also, it normally retrieves the binary format of a class from a ClassLoader, however without instructing it to load this class.

The Java virtual machine only loads a class on its first usage. As a consequence, we can for example safely redefine a class such as

package foo;
class Bar { }

right at program startup before running any other code:

class MyApplication {
  public static void main(String[] args) {
    TypePool typePool = TypePool.Default.ofSystemLoader();
    Class bar = new ByteBuddy()
      .redefine(typePool.describe("foo.Bar").resolve(), // do not use 'Bar.class'
                ClassFileLocator.ForClassLoader.ofSystemLoader())
      .defineField("qux", String.class) // we learn more about defining fields later
      .make()
      .load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION)
      .getLoaded();
    assertThat(bar.getDeclaredField("qux"), notNullValue());
  }
}

By explicitly loading the redefined class before its first use in the assertion statement, we forestall the JVM's built-in class loading. This way, the redefined definition of foo.Bar is loaded and used throughout our application's runtime. Note however that we do not reference the class by a class literal when we use the TypePool to provide a description. If we did use a class literal for foo.Bar, the JVM would have loaded this class before we had a chance to redefine it, and our redefinition attempt would be without effect. Also, note that when working with unloaded classes, we further need to specify a ClassFileLocator which allows to locate a class's .class file. In the example above, we simply create a class file locator which scans the running application's class path for such files.

Creating Java agents

When an application grows bigger and becomes more modular, applying such a transformation at a specific program point is of course a cumbersome constraint to enforce. And there is indeed a better way to apply such class redefinitions on demand. Using a Java agent, it is possible to directly intercept any class loading activity that is conducted within a Java application. A Java agent is implemented as a simple jar file with an entry point that is specified in this jar's manifest file, as it is described under the linked resource. Using Byte Buddy, the implementation of such an agent is straight forward by using an AgentBuilder. Assuming that we defined a simple annotation named ToString as below:

@Retention(RetentionPolicy.RUNTIME)
public @interface ToString {
}

It would be trivial to implement toString methods for all annotated classes simply by implementing the Agent's premain method as follows:

class ToStringAgent {
  public static void premain(String arguments, Instrumentation instrumentation) {
    new AgentBuilder.Default()
        .type(isAnnotatedWith(ToString.class))
        .transform(new AgentBuilder.Transformer() {
      @Override
      public DynamicType.Builder transform(
            DynamicType.Builder builder,
            TypeDescription td, ClassLoader cl, JavaModule jm, ProtectionDomain pd) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }
    }).installOn(instrumentation);
  }
}

The code of the premain method can be even simplified by writing a Lambda Expression if you are using JDK 8+:

new AgentBuilder.Default()
    .type(isAnnotatedWith(ToString.class))
    .transform((builder, td, cl, jm, pd) -> builder.method(named("toString")).intercept(FixedValue.value("transformed")))
    .installOn(instrumentation);

As a result of applying the above AgentBuilder.Transformer, all toString methods of the annotated classes would now return transformed. We will learn all about Byte Buddy's DynamicType.Builder in the upcoming sections, do not worry about this class for now. The above code results of course in a trivial and meaningless application. Using this concept right, renders however a powerful tool for easily implementing aspect-oriented programing.

Annotate the Bar class we created before (or any other one) with @ToString so that we can try our agent. Now you can add a regular main method to the ToStringAgent class, that manually calls the premain method, getting an Instrumentation instance from ByteBuddyAgent.install():

public static void main(String[] args) {
    premain("", ByteBuddyAgent.install());
    System.out.println(new Bar());
}

This way, it tries to install the agent in a running JVM, without requiring you to pass the -javaagent parameter to the JVM. However, that approach may not always work (check the ByteBuddyAgent.install() docs for details).

Note that it is also possible to instrument classes that were loaded by the bootstrap class loader when using an agent. However, this requires some preparation. First of all, the bootstrap class loader is represented by the null value which makes it impossible to load a class in this class loader using reflection. This is however sometimes necessary to load helper classes into the instrumented class's class loader to support the class's implementation. In order to load classes into the bootstrap class loader, Byte Buddy can create jar files and add these files to the bootstrap class loader's load path. To make this possible, it is however required to save these classes to disk. A folder for these classes can be specified using the ClassReloadingStrategy.enableBootstrapInjection method, which also takes an instance of the Instrumentation interface in order to append the classes. Note that all user classes that are used by the instrumented class are also required to be put on the bootstrap search path which is possible using the Instrumentation interface.

Loading classes in Android applications

Android uses a different class file format using dex files which are not in the layout of the Java class file format. Furthermore, with the ART runtime which succeeds the Dalvik virtual machine, Android applications are compiled into native machine code before being installed on an Android device. As a result, Byte Buddy cannot longer redefine or rebase classes as long as an applications is not explicitly deployed together with its Java sources as there is otherwise no intermediate code representation to interpret. Byte Buddy is however still capable to define new classes using a DexClassLoader together with a built-in dex compiler. For this purpose, Byte Buddy offers the byte-buddy-android module which contains the AndroidClassLoadingStrategy which allows the loading of dynamically created classes from within an Android application. In order to function, it requires a folder for writing temporary files and compiled class files. This folder must not be shared among different applications as this is forbidden by Android's security manager.

Working with generic types

Byte Buddy is processing generic types as they are defined by the Java programming language. Generic types are not considered by the Java runtime which only processes the erasures of generic types. However, generic types are still embedded into any Java class file and are exposed by the Java reflection API. Therefore, it sometimes makes sense to include generic information into a generated class because the generic type information can effect the behavior of other libraries and frameworks. Embedding generic type information is also important when a class is persisted and processed as a library by the Java compiler.

When creating a subclass, implementing an interface or declaring a field or method, Byte Buddy accepts a Java Type instead of an erased Class for the above reasons. Generic types can also be defined explicitly by using the TypeDescription.Generic.Builder. One important difference of Java generic types to type erasures is the contextual meaning of type variables. A type variable of a certain name, defined by some type, does not necessarily denote the same type when another type declares the same type variable with the same name. Therefore, Byte Buddy rebinds all generic types that denote type variables in the context of the generated type or method when a Type instance is handed to the library.

Byte Buddy also inserts bridge methods transparently when a type is created. Bridge methods are resolved by a MethodGraph.Compiler which is a property of any ByteBuddy instance. The default method graph compiler behaves like the Java compiler and processes any class file's generic type information. For other languages than Java, a different method graph compiler might however be appropriate.

Fields and methods

Most types we created in the previous section did not define any fields or methods. However, by subclassing Object, the created class inherits the methods that are defined by its super class. Let us verify this Java trivia and call the toString method on an instance of the dynamic type. We can get hold of an instance by calling the created class's constructor reflectively.

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance() // Java reflection API
  .toString();

The implementation of the Object#toString method returns the concatenation of the instance's fully qualified class name and the hex representation of the instance's hash code. And in fact, invoking the toString method on the created instance returns something like example.Type@340d1fa5.

Of course, we are not done here. The main motivation of creating dynamic classes is the ability to define new logic. To demonstrate how this is done, let us start with something simple. We want to override the toString method and return Hello World! instead of the previous default value:

String toString = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .method(named("toString")).intercept(FixedValue.value("Hello World!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .toString();

The line we added to our code contains two instructions in Byte Buddy's domain specific language. The first instruction is method which allows us to select any number of methods that we want to override. This selection is applied by handing over a ElementMatcher which serves as a predicate to decide for each overridable method if it should be overridden or not. Byte Buddy comes with a lot of predefined method matchers which are collected in the ElementMatchers class. Normally, you would import this class statically such that the resulting code reads more naturally. Such a static import was also assumed for the above example where we used the named method matcher which selects methods by their exact names. Note that the predefined method matchers are composable. This way, we could have described the method selection in further detail such as by:

named("toString").and(returns(String.class)).and(takesArguments(0))

This latter method matcher describes the toString method by its full Java signature and therefore only matches this particular method. However, in the given context we know that there is no other method named toString with a different signature such that our original method matcher is sufficient.

After selecting the toString method, the second instruction intercept determines the implementation that should override all methods of the given selection. In order to know how to implement a method, this instruction requires a single argument of type Implementation. In the above example, we are making use of the FixedValue implementation which ships with Byte Buddy. As suggested by this class's name, it implements a method that always return a given value. We will have a more detailed look at the FixedValue implementation a little later in this section. Right now, let us rather look a little closer at the method selection.

So far, we only intercepted a single method. In real applications, things might however be more complicated and we might want to apply different rules for overriding different methods. Let us look at an example of such a scenario:

class Foo {
  public String bar() { return null; }
  public String foo() { return null; }
  public String foo(Object o) { return null; }
}

Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

In the above example, we defined three different rules for overriding methods. When investigating the code, you will notice that the first rule concerns any method that is defined by Foo, i.e. all three methods in the example class. The second rule matches both methods that are named foo, a subset of the previous selection. And the last rule only matches the foo(Object) method which is a further reduction of the former selection. But given this selection overlap, how does Byte Buddy decide which rule is applied to which method?

Byte Buddy organizes rules for overriding methods in a stack form. This means that whenever you register a new rule for overriding a method, ít is pushed on the top of this stack and is always applied first until a new rule is added which will then be of even higher priority. For the above example, this means that:

  • The bar() method is first matched against named("foo").and(takesArguments(1)) and then against named("foo") where both matching attempts turn out negative. Finally, the isDeclaredBy(Foo.class) matcher gives green light to override the bar() method to return One!.
  • Similarly, the foo() method is first matched against named("foo").and(takesArguments(1)) first where the missing argument results in an unsuccessful matching. After this, the named("foo") matcher determines a positive match such that the foo() method is overridden to return Two!.
  • The foo(Object) is immediately matched by the named("foo").and(takesArguments(1)) matcher such that the overridden implementation returns Three!.

Because of this organization, you should always register more specific method matchers last. Otherwise, any less specific method matcher that is registered afterward might prevent rules that you defined before from being applied. Note that the ByteBuddy provides an ignoreAlso method that also takes a matcher, so that methods that are successfully matched are never overridden. By default, Byte Buddy does not override any synthetic methods.

In some scenarios, you might want to define a new method that does not override a method of a super type or an interface. This is also possible using Byte Buddy. For this purpose, you can call define(Method) or defineMethod(name, ...) where you are able to define a signature. After defining a method, you are asked to provide an Implementation just as with a method that was identified by a method matcher. Note that method matchers that are registered after a method's definition might supersede this implementation by the stacking principle that we discussed before.

With define(Field) or defineField(name, ...), Byte Buddy allows to define fields for a given type. In Java, fields are never overridden but can only be shadowed. For this reason, no field matching or the like is available.

With this knowledge on how methods are selected, we are ready to learn about how we can implement these methods. For this purpose, we now look at predefined Implementation implementations that ship with Byte Buddy. Defining custom implementation is discussed in its own section and is only intended for users that require very custom method implementations.

A closer look at fixed values

We have already seen the FixedValue implementation in action. As the name suggests, methods that are implemented by FixedValue simply return a provided object. A class is able to remember such an object in two different manners:

  • The fixed value is written to a class's constant pool. The constant pool is a section within the Java class file format and contains numerous stateless values that describe the properties of any class. The constant pool is mainly required to remember a class's property such as the class's name or the names of its methods. Besides these reflective properties, the constant pool has room for storing any string or primitive value that is used within a method or a field of the class. Besides strings and primitive values, the class pool can also store references to other types.
  • The value is stored in a static field of the class. For this to happen, the field must however be assigned the given value once the class was loaded into the Java virtual machine. For this purpose, every dynamically created class is accompanied by a TypeInitializer which can be configured to execute such explicit initialization. When you instruct a DynamicType.Unloaded to be loaded, Byte Buddy automatically triggers its type initializer such that the class is ready for use. Therefore, you do not normally need to worry about type initializers. However, if you want dynamic classes to be loaded outside of Byte Buddy, it is important that you run their type initializers manually after these classes are loaded. Otherwise, a FixedValue implementation would for example return null instead of the required value because the static field was never assigned this value. Many dynamic types might however not require explicit initialization. A class's type initializer can therefore be queried for its liveliness by calling its isAlive method. If you need to trigger a TypeInitializer manually, you find it exposed by the DynamicType interface.

When you implement a method by FixedValue#value(Object), Byte Buddy analyzes the parameter's type and define it to be stored in (i) the class pool of the dynamic type, if possible, or (ii) in a static field otherwise. Note however that the instance that is returned by the selected methods might be of a different object identity if the value was stored in the class pool. Therefore, you can instruct Byte Buddy to always store an object in a static field by using FixedValue#reference(Object). The latter method is overloaded such that you can provide the field's name as a second argument. Otherwise, a field name is derived automatically from the object's hash code. An exception from this behavior is the null value. The null value is never stored in a field but is simply represented by its literal expression.

You might wonder about type safety in this context. Obviously, you could define a method to return an invalid value:

new ByteBuddy()
  .subclass(Foo.class)
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0))
  .make();

It would be difficult to prevent this invalid implementation by the compiler within Java's type system. Instead, Byte Buddy will throw an IllegalArgumentException when the type is created and the illegal assignment of an integer to a method that returns a String becomes effective. Byte Buddy tries its best to assure that all its created types are legal Java types and fail fast by throwing an exception during the creation of an illegal type.

Byte Buddy's assignment behavior is customizable. Again, Byte Buddy only provides a sane default which mimics the assignment behavior of the Java compiler. Consequently, Byte Buddy allows an assignment of a type to any of its super types and it will also consider to box primitive values or to unbox their wrapper representations. Note however, that Byte Buddy does currently not fully support generic types and will only consider type erasures. Therefore, it is possible that Byte Buddy causes heap pollution. Instead of using the predefined assigner, you can always implement your own Assigner which is capable of type transformations that are not implicit in the Java programming language. We will look into such custom implementations in the last section of this tutorial. For now, we settle for mentioning that you can define such custom assigners by calling withAssigner on any FixedValue implementation.

Delegating a method call

In many scenarios, returning a fixed value from a method is of course insufficient. For more flexibility, Byte Buddy provides the MethodDelegation implementation which offers maximal freedom in reacting to method calls. A method delegation makes a method of the dynamically created type to forward any call to another method which may live outside the dynamic type. When providing a reference to the target class where a method call will be delegated to, the target methods must be static for the delegation to work (instance methods are also supported, as detailed further). This way, a dynamic class's logic can be represented using plain Java, while only the binding to another method is achieved by code generation. Before discussing the details, let us look at an example of using a MethodDelegation:

class Source {
  public String hello(String name) { return null; }
}

class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}

String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");

In the example, we are delegating invocations of the Source#hello(String) method to the Target type, such that the method returns Hello World! instead of null. For this purpose, the MethodDelegation implementation identifies an invokable method of the Target type that best matches the Source method. In the above example, this is trivial since the Target type only defines a single static method where the method's parameters, return type and name are conveniently identical to those of Source#name(String).

In reality, the decision for a delegation target method will most likely be more complex. So how does Byte Buddy decide between methods if there is an actual choice? For this purpose, let us assume the Target class to be defined as follows:

class Target {
  public static String intercept(String name) { return "Hello " + name + "!"; }
  public static String intercept(int i) { return Integer.toString(i); }
  public static String intercept(Object o) { return o.toString(); }
}

You might have noticed that the above methods are now all called intercept. Byte Buddy does not require target methods to be named equally to a source method. We will look closer into this matter shortly. More importantly, if you ran the former example with the altered definition of Target, you would observe that the hello(String) method was bound to intercept(String). But why is that? Obviously, the intercept(int) method cannot receive the String argument of the source method and is therefore not even considered as a possible match. However, this is not true for the intercept(Object) method which could be bound. In order to resolve this ambiguity, Byte Buddy once again mimics the Java compiler by choosing the method binding with the most specific parameter types. Remember how the Java compiler chooses a binding for an overloaded method! Since String is more specific than Object, the intercept(String) class is finally chosen among the three alternatives.

With the information so far, you might consider the method binding algorithm to be of a rather rigid nature. However, we have not yet told the full story. So far we only observed another example of the convention over configuration principle, which is open for change if the defaults do not fit the actual requirements. In reality, the MethodDelegation implementation works with annotations where a parameter's annotation decides which value should be assigned to it. However, if no annotation is found, Byte Buddy treats a parameter as if it was annotated with @Argument. This latter annotation causes Byte Buddy to assign the n-th argument of the source method to the annotated target. When the annotation is not added explicitly, the value of n is set to the annotated parameter's index. According to this rule, Byte Buddy treats

void foo(Object o1, Object o2)

as if the all parameters were annotated as:

void foo(@Argument(0) Object o1, @Argument(1) Object o2)

As a result, the first and the second argument of the instrumented method are assigned to the interceptor. If the intercepted method does not declare at least two parameters or if the annotated parameter types are not assignable from the instrumented method's parameter types, the interceptor method in question is discarded.

Besides the @Argument annotation, there are several other pre-defined annotations that can be used with a MethodDelegation:

  • Parameters that carry the @AllArguments annotation (from net.bytebuddy.implementation.bind.annotation) must be of an array type and are assigned an array containing all the source method's arguments. For this purpose, all source method parameters must be assignable to the array's component type. If this is not the case, the current target method is not considered as a candidate for being bound to the source method.
  • The @This annotation induces the assignment of the dynamic type's instance on which the intercepted method is currently invoked. If the annotated parameter is not assignable to an instance of the dynamic type, the current method is not considered as a candidate for being bound to the source method. Note that calling any methods on this instance will result in calling a potentially instrumented method. For calling the original implementations, you need to use the @Super annotation which is discussed below. A typical reason for using the @This annotation is to gain access to an instance's fields.
  • Parameters that are annotated with @Origin must be declared using one of the following types: Method, Constructor, Class, Executable, MethodHandle, MethodType, String or int. Depending on the parameter's type, it is assigned a different value to it, as described in the table below.
    Parameter typeAssigned value
    Method, Constructor or Class Reference to the original method or constructor (that is now instrumented) or the dynamically created class.
    Executable A method or constructor reference (requires Java 8+).
    String The value that the Method#toString() would have returned.
    MethodHandle An object that enables calling the original method.
    MethodTypeAn object that represents the signature of the MethodHandle.
    intThe modifier of the instrumented method.
    In general, we recommend the use of these String values as method identifiers wherever possible and discourage the use of Method objects as their lookup introduces a significant runtime overhead. To avoid this overhead, the @Origin annotation also offers a property for caching such instances for reuse. Note that the MethodHandle and MethodType are stored in a class's constant pool such that classes using these constants must at least be of Java version 7. Instead of using reflection for reflectively invoking an intercepted method on another object, we furthermore recommend the use of the @Pipe annotation which is discussed later in this section.

Byte Buddy also allows you to define your own annotations by registering one or several ParameterBinders. We will look into such customization in the last section of this tutorial.

Accessing super implementations

Besides the four annotation we have discussed so far, there exist two other predefined annotations that grant access to the super implementations of a dynamic type's methods. This way, a dynamic type could for example add aspects to a class such as the logging of method invocations. Using the @SuperCall annotation, an invocation of the super implementation of a method can be executed even from outside the dynamic class as demonstrated in the following example:

class MemoryDatabase {
  public List<String> load(String info) {
    return Arrays.asList(info + ": foo", info + ": bar");
  }
}

class LoggerInterceptor {
  public static List<String> log(@SuperCall Callable<List<String>> zuper)
      throws Exception {
    System.out.println("Calling database");
    try {
      return zuper.call();
    } finally {
      System.out.println("Returned from database");
    }
  }
}

MemoryDatabase loggingDatabase = new ByteBuddy()
  .subclass(MemoryDatabase.class)
  .method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();

From the above example, it is obvious that the super method is called by injecting some instance of a Callable into the LoggerInterceptor, which invokes the original non-overridden implementation of MemoryDatabase#load(String) from its call method. Byte Buddy creates a helper class that implements Callable. This class is called an AuxiliaryType within Byte Buddy's terminology. Auxiliary types are created on demand by Byte Buddy and are directly accessible from the DynamicType interface after a class was created. Because of such auxiliary types, the manual creation of one dynamic type might result in the creation of several additional types which aid the implementation of the original class. Finally, note that the @SuperCall annotation can also be used on the Runnable type where the original method's return value is however dropped.

You might still wonder how this auxiliary type is able to call a super method of another type, which is normally forbidden in Java. On closer inspection, this behavior is however quite common and resembles the compiled code that is generated when the following Java source code snippet gets compiled:

class LoggingMemoryDatabase extends MemoryDatabase {

  private class LoadMethodSuperCall implements Callable {

    private final String info;
    private LoadMethodSuperCall(String info) {
      this.info = info;
    }

    @Override
    public Object call() throws Exception {
      return LoggingMemoryDatabase.super.load(info);
    }
  }

  @Override
  public List<String> load(String info) {
    return LoggerInterceptor.log(new LoadMethodSuperCall(info));
  }
}

Sometimes, you might however want to call a super method with different arguments than those that were assigned on the method's original invocation. This is also possible in Byte Buddy by using the @Super annotation. This annotation triggers the creation of another AuxiliaryType which now extends a super class or an interface of the dynamic type in question. Similar to before, the auxiliary type overrides all methods to call their super implementations on the dynamic type. This way, the logger interceptor from the previous example can be implemented to change the actual invocation:

class ChangingLoggerInterceptor {
  public static List<String> log(String info, @Super MemoryDatabase zuper) {
    System.out.println("Calling database");
    try {
      return zuper.load(info + " (logged access)");
    } finally {
      System.out.println("Returned from database");
    }
  }
}

Note that the instance that is assigned to the parameter annotated with @Super is of a different identity to the actual instance of the dynamic type! Therefore, no instance field that is accessible by means of the parameter reflects the actual instance's field. Furthermore, non-overridable methods of the auxiliary instance do not delegate their invocations. They retain the original implementation that can result in absurd behavior when they are invoked. Finally, in case a parameter annotated with @Super does not represent a super type of the relevant dynamic type, the method is not considered as a binding target for any of its methods.

Because the @Super annotation allows the use of any type, we might be required to provide information on how this type can be constructed. By default, Byte Buddy attempts to use a class's default constructor. This always works for interfaces which implicitly extend the Object type. However, when extending a super class of the dynamic type, this class might not even provide a default constructor. If this is the case or if a specific constructor should be used for creating such an auxiliary type, the @Super annotation allows to identify a different constructor by setting its parameter types as the annotation's constructorParameters property. This constructor will then be called by assigning the corresponding default value to each parameter. Alternatively, it is also possible to set the strategy property to Super.Instantiation.UNSAFE for instantiating the auxiliary type using Java internal mechanisms, which does not invoke any constructor. However, note that this strategy is not necessarily portable to non-Oracle JVMs and might no longer be available in future JVM releases. As of today, the internal classes that are used by this unsafe instantiation strategy are however found in almost any JVM implementation.

Checked exceptions

You might already have noticed that the above LoggerInterceptor declares a checked Exception. On the other hand, the instrumented source method which invokes this method does not declare any checked exception. Usually, the Java compiler would refuse to compile such an invocation. However, in contrast to the compiler, the Java runtime does not treat checked exceptions differently than their unchecked counterparts and permits this invocation. For this reason, we decided to ignore checked exceptions and grant full flexibility in their use. However, be careful when throwing undeclared checked exceptions from dynamically created methods since the encounter of such an exception might confuse the users of your application.

Dynamic types for interceptor parameters

There is another caveat in the method delegation model that might have come to your attention. While static typing is great for implementing methods, strict types can limit the reuse of code. To understand why, consider the following example:

class Loop {
  public String loop(String value) { return value; }
  public int loop(int value) { return value; }
}

Because the methods of the above class describe two similar signatures with incompatible types, you would not usually be able to instrument both methods by using a single interceptor method. Instead, you would have to provide two different target methods with different signatures only to satisfy the static type check. To overcome this limitation, Byte Buddy allows to annotate methods and method parameters with @RuntimeType, which instructs Byte Buddy to suspend the strict type check in favor of a runtime type casting:

class Interceptor {
  @RuntimeType
  public static Object intercept(@RuntimeType Object value) {
    System.out.println("Invoked method with: " + value);
    return value;
  }
}

Using the above target method, we are now able to provide a single interception method for both source methods. Note that Byte Buddy is also able to box and unbox primitive values. However, be aware that the use of @RuntimeType comes at the cost of abandoning type safety, and you might end up with a ClassCastException if you get incompatible types mixed up.

Delegation to an interface's default method

As an equivalent to @SuperCall, Byte Buddy comes with a @DefaultCall annotation which allows the invocation of a default method instead of calling a method's super method. A method with this parameter annotation is only considered for binding if the intercepted method is, as a matter of fact, declared as a default method by an interface that is directly implemented by the instrumented type. Similarly, a @SuperCall annotation prevents a method's binding if the instrumented method does not define a non-abstract super method. If you however want to invoke a default method on a specific type, you can specify the @DefaultCall's targetType property with a specific interface. With this specification, Byte Buddy injects a proxy instance which invokes the given interface type's default method, if such a method exists. Otherwise, the target method with the parameter annotation is not considered as a delegation target. Obviously, default method invocation is only available for classes that are defined in a class file version equal to Java 8 or newer. Similarly, in addition to the @Super annotation, there is a @Default annotation which injects a proxy for invoking a specific default method explicitly.

Forwarding method invocations

We already mentioned that you can define and register custom annotations with any MethodDelegation. Byte Buddy comes with one annotation that is ready for use, but still needs to be installed and registered explicitly. By using the @Pipe annotation, you can forward an intercepted method invocation to another instance. The @Pipe annotation is not preregistered with the MethodDelegation because the Java class library does not come with a suitable interface type before Java 8 which defines the Function type. Therefore, you need to explicitly provide a type with a single non-static method which takes an Object as its argument and returns another Object as a result. Note that you can still use a generic type as long as the method types are bound by the Object type. Of course, if you are using Java 8, the Function type is a feasible option. When invoking the method on the parameter's argument, Byte Buddy casts the parameter to the method's declaring type and invokes the intercepted method with the same arguments as the original method call. Before we look at an example, let us however define a custom type which you can use with Java {{javaVersion}} to 7 (since from Java 8+ you can use the Function interface):

interface Forwarder<T, S> {
  T to(S target);
}

Using this type, we can now implement a new solution to logging access of the above MemoryDatabase by forwarding a method invocation to an existing instance:

class ForwardingLoggerInterceptor {
  private final MemoryDatabase memoryDatabase;

  // constructor omitted

  public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) {
    System.out.println("Calling database");
    try {
      return pipe.to(memoryDatabase);
    } finally {
      System.out.println("Returned from database");
    }
  }
}

MemoryDatabase loggingDatabase = new ByteBuddy()
                .subclass(MemoryDatabase.class)
                .method(named("load")).intercept(MethodDelegation.withDefaultConfiguration()
                                                                 .withBinders(Pipe.Binder.install(Forwarder.class))
                .to(new ForwardingLoggerInterceptor(new MemoryDatabase())))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();

loggingDatabase.load("Hello");

In the above example, we only forward the invocation to another instance that we create locally. However, the advantage over intercepting a method by subclassing a type, is that this approach allows to enhance an already existing instance. Furthermore, you would normally register an interceptor on the instance level instead of registering a static interceptor on the class level.

Ambiguity resolver for selection of target methods

So far, we have seen a great deal of the MethodDelegation implementation. But before we proceed, we want to take a more detailed look on how Byte Buddy selects a target method. We already described how Byte Buddy resolves a most specific method by comparing parameter types, but there is more to it. After Byte Buddy identified candidate methods that qualified for a binding to a given source method, it delegates the resolution to a chain of AmbiguityResolvers. Again, you are free to implement your own ambiguity resolvers that can complement or even replace Byte Buddy's defaults. Without such alterations, the ambiguity resolver chain attempts to identify a unique target method by applying the following rules in the same order as below:

  • Methods can be assigned an explicit priority by annotating them with @BindingPriority. If a method is of a higher priority than another method, the high priority method is always preferred over that with lower priority. In addition, a method that is annotated by @IgnoreForBinding is never considered as a target method.
  • If a source method and a target method have an identical name, this target method is preferred over other target methods that have a different name.
  • If two methods bind the same parameters of a source method by using @Argument, the method with the most specific parameter types is considered. In this context, it does not matter if the annotation is provided explicitly or implicitly by not annotating a parameter. The resolution algorithm works similar to the Java compiler's algorithm for resolving calls to overloaded methods. If two types are equally specific, the method that binds more arguments is considered as a target. If a parameter should be assigned an argument without considering the parameter type at this resolution stage, this is possible by setting the annotation's bindingMechanic attribute to BindingMechanic.ANONYMOUS. Furthermore, note that non-anonymous parameters need to be unique per index value on each target method for the resolution algorithm to work.
  • If a target method has more parameters than another target method, the former method is preferred over the latter.
Delegating calls to instance methods

So far, we only delegated method invocations to static methods by naming a specific class as in MethodDelegation.to(Target.class). It is however also possible to delegate to instance methods or to constructors:

  • By calling MethodDelegation.to(new Target()), it is possible to delegate method invocations to any of the instance methods of the Target class. Note that this includes methods that are defined anywhere in the instance's class hierarchy, including the methods that are defined in the Object class. You might want to filter the range of possible candidate methods by calling MethodDelegation.withDefaultConfiguration().filter(ElementMatcher). The ElementMatcher type is the same that was used before for selecting source methods within Byte Buddy's domain specific language. The instance which is the target of the method delegation is stored in a static field. Similarly to the definition of fixed values, this requires the definition of a TypeInitializer. Instead of storing a delegation in a static field, you can alternatively define the use of any field by MethodDelegation.toField(String) where the argument specifies a field name to which all method delegations are forwarded. Always remember to assign a value to this field before calling methods on an instance of such a dynamic class. Otherwise, a method delegation will result in a NullPointerException.
  • A method delegation can be used to construct instances of a given type. By using MethodDelegation.toConstructor(Class), any invocation of an intercepted method returns a new instance of the given target type.
Independence from Byte Buddy

As you just learned, the MethodDelegation inspects annotations for adjusting its binding logic. These annotations are specific to Byte Buddy but this does not mean that the annotated classes become in any way dependent on Byte Buddy. Instead, the Java runtime simply ignores annotation types that cannot be found on the class path when a class is loaded. This implies that Byte Buddy is no longer required after a dynamic class is created. This means that you could load the dynamic classes and the types to which it delegates its method calls in another JVM process, even without having Byte Buddy on the class path.

Other MethodDelegation annotations

There are several more predefined annotations that can be used with a MethodDelegation that we only want to name briefly. If you want to read more about these annotations, you can find further information in the in-code documentation. These annotations are:

  • @Empty: Applying this annotation, Byte Buddy injects the parameter type's default value. For primitive types, this is the equivalent of the number zero, for reference types, this is null. Using this annotation is meant for voiding an interceptor's parameter.
  • @StubValue: With this annotation, the annotated parameter is injected a stub value of the intercepted method. For reference-return-types and void methods, the value null is injected. For methods that return a primitive value, the equivalent boxing type of 0 is injected. This can be helpful in combination when defining a generic interceptor that returns a Object type while using a @RuntimeType annotation. By returning the injected value, the method behaves as a stub while correctly considering primitive return types.
  • @FieldValue: This annotation locates a field in the instrumented type's class hierarchy and injects the field's value into the annotated parameter. If no visible field of a compatible type can be found for the annotated parameter, the target method is not bound.
  • @FieldProxy: Using this annotation, Byte Buddy injects an accessor for a given field. The accessed field can either be specified explicitly by its name or it is derived from a getter or setter methods name, in case that the intercepted method represents such a method. Before this annotation can be used, it needs to be installed and registered explicitly, similarly to the @Pipe annotation.
  • @Morph: This annotation works very similar to the @SuperCall annotation. However, using this annotation allows to specify the arguments that should be used for invoking the super method. Note that you should only use this annotation when you need to invoke a super method with different arguments than the original invocation, since using the @Morph annotation requires a boxing and unboxing of all arguments. If you want to invoke a specific super method, consider using the @Super annotation for creating a type-safe proxy. Before this annotation can be used, it needs to be installed and registered explicitly, similarly to the @Pipe annotation.
  • @SuperMethod: This annotation can only be used on parameter types that are assignable from Method. The assigned method is set to be a synthetic accessor method that allows for the invocation of the original code. Note that using this annotation causes a public accessor to be created for the proxy class that allows for the outside invocation of the super method without passing a security manager.
  • @DefaultMethod: Similar to @SuperMethod but for a default method call. The default method is invoked on a unique type if there is only one possibility for a default method invocation. Otherwise, a type can be specified explicitly as an annotation property.

Calling a super method

As the name suggests, the SuperMethodCall implementation (do not confuse this class with the @SuperCall annotation) can be used to invoke a method's super implementation. At first glance, the sole invocation of a super implementation does not seem very useful since this will not change an implementation but only replicate existent logic. However, by overriding a method, you are able to change the annotations of a method and its parameters, something we will look into in the next section. Another rationale for calling a super method in Java is however the definition of a constructor which must always invoke another constructor of either its super type or its own type.

So far we simply assumed that the constructors of a dynamic type would always resemble the constructors of its direct super type. As an example, we could call

new ByteBuddy()
  .subclass(Object.class)
  .make()

to create a subclass of Object with a single default constructor which is defined to simply invoke its direct super constructor, the default constructor of Object. However, this behavior is not stipulated by Byte Buddy. Instead, the above code is a shortcut for calling

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS)
  .make()

where a ConstructorStrategy is responsible for creating a set of predefined constructors for any given class. Besides the above strategy, which copies each visible constructor of a dynamic type's direct super class, there exist three other predefined strategies: (i) one that does not create any constructor at all, (ii) one that creates a default constructor, which is invoking the direct super class's default constructor and throws an exception if no such constructor exists, and (iii) one that only imitates public constructors of the super type.

Within the Java class file format, constructors do not generally differ from methods such that Byte Buddy allows them to be treated just as such. However, constructors are required to contain a hard-coded invocation of another constructor to be accepted by the Java runtime. For this reason, most predefined implementations besides SuperMethodCall will fail to create a valid Java class when applied to a constructor.

However, by using custom implementations, you are able to define your own constructors by either implementing a custom ConstructorStrategy or by defining an individual constructor within Byte Buddy's domain specific language using the defineConstructor method.

For class rebasing and class redefinition, constructors are of course simply retained, which makes the specification of a ConstructorStrategy obsolete. Instead, for copying these retained constructors' (and methods') implementations, it is required to specify a ClassFileLocator which allows a lookup of the original class file that contains these constructor definitions. Byte Buddy does its best to identify the location of the original class file by itself, e.g. by querying the corresponding ClassLoader or by looking on an application's class path. When dealing with customary class loader, a lookup might however still not be successful. Then, a custom ClassFileLocator can be provided.

Calling a default method

With its version 8 release, the Java programming language introduced default methods for interfaces. In Java, a default method invocation is expressed by a similar syntax to the invocation of a super method. As an only disparity, a default method invocation names the interface that defines the method. This is necessary because a default method invocation can be ambiguous if two interfaces define a method with identical signature. Accordingly, Byte Buddy's DefaultMethodCall implementation (do not confuse this class with the @DefaultCall annotation) takes a list of prioritized interfaces. When intercepting a method, the DefaultMethodCall invokes a default method on the first-mentioned interface. As an example, assume that we wanted to implement the two following interfaces:

interface First {
  default String qux() { return "FOO"; }
}

interface Second {
  default String qux() { return "BAR"; }
}

If we now created a class that implements both interfaces and implemented the qux method to call a default method, this invocation could express both a call of the default method defined on the First or the Second interface. However, by specifying the DefaultMethodCall to prioritize the First interface, Byte Buddy would know that it should invoke this latter interface's method instead of the alternative.

new ByteBuddy(ClassFileVersion.JAVA_V8)
  .subclass(Object.class)
  .implement(First.class)
  .implement(Second.class)
  .method(named("qux")).intercept(DefaultMethodCall.prioritize(First.class))
  .make()

Note that any Java class that is defined in a class file version before Java 8 does not support default methods. Furthermore, you should be aware that Byte Buddy imposes weaker requirements on the invokability of a default method compared to the Java programming language. Byte Buddy only requires a default method's interface to be implemented by the most-specific class in a type's hierarchy. Other than the Java programming language, it does not require this interface to be the most specific interface that is implemented by any super class. Finally, if you do not expect an ambiguous default method definitions, you can always use DefaultMethodCall.unambiguousOnly() for receiving an implementation which throws an exception on the discovery of an ambiguous default method invocation. This same behavior is displayed by a prioritizing DefaultMethodCall where a default method call is ambiguous between non-prioritized interfaces and no prioritized interface was found to define a method with a compatible signature.

Calling a specific method

In some cases, the above Implementations are not sufficient to implement more custom behavior. For example, one might want to implement a custom class with explicit behavior. For example, we might want to implement the following Java class with a constructor that does not have a super constructor with identical arguments:

public class SampleClass {
  public SampleClass(int unusedValue) {
    super();
  }
}

The previous SuperMethodCall implementation could not be used to implement this class as the Object class does not define a constructor that takes an int as its parameter. Instead, we can invoke the Object super constructor explicitly:

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
  .defineConstructor(Visibility.PUBLIC)
  .withParameters(int.class)
  .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()))
  .make();

With the above code, we have created a simple subclass of Object that defines a single constructor which takes a single int parameter that is not used. The latter constructor is then implemented by an explicit method call to the Object super constructor.

The MethodCall implementation can also be used when passing arguments. These arguments are either passed explicitly as a value, as a value for an instance field that needs to be set manually or as a given parameter value. Also, the implementation allows to invoke methods on other instances than the one being instrumented. Furthermore, it allows for the construction of new instances to be returned from an intercepted method. The documentation of the MethodCall class provides detailed information on these features.

Accessing fields

Using the FieldAccessor, it is possible to implement a method to read or to write a field value. In order to be compatible to this implementation, a method must either:

  • Have a signature similar to void setBar(Foo f) to define a field setter. The setter will normally access a field named bar, as it is conventional in the Java bean specification. In this context, the parameter type Foo must be a subtype of this field's type.
  • Have a signature similar to Foo getBar() to define a field getter. The getter will normally access a field named bar, as it is conventional in the Java bean specification. For this to be possible, the method's return type Foo must be a super type of the field's type.

Creating such an implementation is trivial: Simply call FieldAccessor.ofBeanProperty(). However, if you do not want to derive a field's name from a method's name, you can still specify the field name explicitly by using FieldAccessor.ofField(String). Using this method, the only argument defines the field's name that should be accessed. If required, this even allows you to define a new field if such a field does not yet exist. When accessing an existing field, you are able to specify the type in which a field is defined by calling the in method. In Java, it is legal to define a field in several classes of a hierarchy. In the process, a field of a class is shadowed by the field definition in its subclass. Without such an explicit location of the field's class, Byte Buddy will access the first field it encounters by traversing through a class hierarchy, starting with the most specific class.

Let us look at an example application of the FieldAccessor. For this example, we assume that we receive some UserType that we want to subclass at runtime. For this purpose, we want to register an Interceptor for each instance which is represented by an interface. This way, we are able to provide different implementations according to our actual requirement. This latter implementation should then be exchangeable by calling methods of the InterceptionAccessor interface on the corresponding instance. In order to create instances of this dynamic type, we further do not want to use reflection, but call a method of an InstanceCreator which serves as an object factory. The following types resemble this setup:

class UserType {
  public String doSomething() { return null; }
}

interface Interceptor {
  String doSomethingElse();
}

interface InterceptionAccessor {
  Interceptor getInterceptor();
  void setInterceptor(Interceptor interceptor);
}

interface InstanceCreator {
  Object makeInstance();
}

We already learned how to intercept methods of a class by using a MethodDelegation. Using the latter implementation, we can define a delegation to an instance field and name this field interceptor. Additionally, we are implementing the InterceptionAccessor interface and intercept all methods of the interface to implement accessors of this field. By defining a bean property accessor, we achieve a getter for getInterceptor and a setter for setInterceptor:

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.toField("interceptor"))
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE)
  .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty())
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();

With the new dynamicUserType, we can implement the InstanceCreator interface to become a factory of this dynamic type. Again, we are using the already known MethodDelegation to call the dynamic type's default constructor:

InstanceCreator factory = new ByteBuddy()
  .subclass(InstanceCreator.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.toConstructor(dynamicUserType))
  .make()
  .load(dynamicUserType.getClassLoader())
  .getLoaded().newInstance();

Note that we need to use the dynamicUserType's class loader to load the factory. Otherwise, this type would not be visible to the factory when it is loaded.

With these two dynamic types, we can finally create a new instance of the dynamically enhanced UserType and define custom Interceptors for its instances. Let us conclude this example by applying some HelloWorldInterceptor to a freshly created instance. Note how we are now able to do this without using any reflection, thanks to both the field accessor interface and the factory.

class HelloWorldInterceptor implements Interceptor {
  @Override
  public String doSomethingElse() {
    return "Hello World!";
  }
}

UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());

Miscellaneous

Additionally, to the Implementations we discussed so far, Byte Buddy includes several other implementations:

  • A StubMethod implements a method to simply return the method return type's default value without any further action. This way, a method call can be silently suppressed. This approach can for example be used to implement mock types. The default value of any primitive type is zero or the zero character respectively. Methods that return a reference type return null as their default.
  • The ExceptionMethod can be used to implement a method to only throw an exception. As mentioned before, it is possible to throw checked exceptions from any method even if a method does not declare this exception.
  • The MethodCall implementation allows to simply forward a method call to another instance of the same type as the declaring type of an intercepted method. The same result can be achieved using a MethodDelegation. However, by MethodCall a simpler delegation model is applied which can cover use cases where no target method discovery is required.
  • The InvocationHandlerAdapter allows to use existing InvocationHandlers from the proxy classes that ship with the Java Class Library.
  • The InvokeDynamic implementation allows to bind a method dynamically at runtime using bootstrap methods which are accessible from Java 7 onwards.

Annotations

We just learned how Byte Buddy relies on annotations for providing some of its functionality. And Byte Buddy is by far not the only Java application with an annotation-based API. In order to integrate dynamically created types with such applications, Byte Buddy allows to define annotations for its created types and their members. Before looking into the details of assigning annotations to dynamically created types, let us look at an example of annotating a runtime class:

@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeDefinition {
}

class RuntimeDefinitionImpl implements RuntimeDefinition {
  @Override
  public Class<? extends Annotation> annotationType() {
    return RuntimeDefinition.class;
  }
}

new ByteBuddy()
  .subclass(Object.class)
  .annotateType(new RuntimeDefinitionImpl())
  .make();

As insinuated by Java's @interface keyword, annotations are internally represented as interface types. As a consequence, annotations can be implemented by a Java class just like an ordinary interface. The only difference to implementing an interface is an annotation's implicit annotationType method which determines the annotation type a class represents. The latter method usually returns the implemented annotation type's class literal. Other than that, any annotation property is implemented as if it was an interface method. However, note that an annotation's default values need to be repeated by an annotation method's implementation.

Defining annotations for a dynamically created class can be particularly important when a class should serve as a subclass proxy for another class. A subclass proxy is often used to implement cross-cutting concerns, where the subclass should mimic the original class as transparently as possible. However, annotations on a class are not retained for its subclasses as long as this behavior is explicitly required by defining an annotation to be @Inherited. Using Byte Buddy, creating subclass proxies that retain their base class's annotations is easy by invoking the attribute method of Byte Buddy's domain specific language. This method expects a TypeAttributeAppender as its argument. A type attribute appender offers a flexible way for defining the annotations of a dynamically created class, based on its base class. For example, by passing a TypeAttributeAppender.ForInstrumentedType, a class's annotations are copied to its dynamically created subclasses. Note that annotations and type attribute appenders are additive and no annotation type must be defined more than once for any class.

Annotating methods and fields

Method and field annotations are defined similarly to type annotations which we just discussed. A method annotation can be defined as a conclusive statement in Byte Buddy's domain specific language for implementing a method. Likewise, a field can be annotated after its definition. Again, let us look at an example:

new ByteBuddy()
  .subclass(Object.class)
    .annotateType(new RuntimeDefinitionImpl())
  .method(named("toString"))
    .intercept(SuperMethodCall.INSTANCE)
    .annotateMethod(new RuntimeDefinitionImpl())
  .defineField("foo", Object.class)
    .annotateField(new RuntimeDefinitionImpl())

The above code example overrides the toString method and annotates the overridden method with RuntimeDefinition. Furthermore, the created type defines a field foo that carries the same annotation and also defines the latter annotation on the created type itself.

By default, a ByteBuddy configuration does not predefine any annotations for a dynamically created type or type member. However, this behavior can be altered by providing a default TypeAttributeAppender, MethodAttributeAppender or FieldAttributeAppender. Note that such default appenders are not additive but replace their former values.

Sometimes, it is desirable to not load an annotation type or the types of any of its properties when defining a class. For this purpose, it is possible to use the AnnotationDescription.Builder (as a parameter to annotateType(), annotateMethod() or annotateField() methods), which offers a fluent interface for defining an annotation without triggering class loading, but at the costs of type safety. All annotation properties are however evaluated at runtime.

By default, Byte Buddy includes any property of an annotation into a class file, including default properties that are specified implicitly by a default value. This behavior can however be customized by providing an AnnotationRetention to a ByteBuddy instance, by calling for instance new ByteBuddy().with(AnnotationRetention.DISABLED).

Type annotations

Byte Buddy exposes and writes type annotations as they were introduced as a part of Java 8. Type annotations are accessible as declared annotations by any TypeDescription.Generic instance. If a type annotation should be added to a type of a generic field or method, an annotated type can be generated using a TypeDescription.Generic.Builder.

Attribute appenders

A Java class file can include any custom information as a so-called attribute. Such attributes can be included by using a Byte Buddy's *AttributeAppender instance for a type, field or method. Attribute appenders can however also be used for defining methods based on information that is provided by the intercepted type, field or method. For example, it is possible to copy all annotations of an intercepted method when overriding a method in a subclass:

class AnnotatedMethod {
  @SomeAnnotation
  void bar() { }
}
new ByteBuddy()
  .subclass(AnnotatedMethod.class)
  .method(named("bar"))
  .intercept(StubMethod.INSTANCE)
  .attribute(MethodAttributeAppender.ForInstrumentedMethod.INCLUDING_RECEIVER)

The above code overrides the bar method of the AnnotatedMethod class but copies all annotations of the overridden method, including annotations on parameters or types.

When a class is redefined or rebased, the same rule might not apply. By default, ByteBuddy is configured to preserve any annotations of a rebased or redefined method, even if the method is intercepted as above. This behavior can however be changed such that Byte Buddy discards any preexisting annotations by setting the AnnotationRetention strategy to DISABLED.

Custom method implementations

In the previous sections, we described Byte Buddy's standard API. None of the features described so far requires knowledge or the explicit expression of Java byte code. However, if you need to create custom byte code, you can do so by directly accessing the API of ASM, a low-level byte code library on top of which Byte Buddy is built. However, note that different versions of ASM are not compatible to another such that you need to repackage Byte Buddy into your own namespace when releasing your code. Otherwise, your application might introduce incompatibilities to other uses of Byte Buddy when another dependency is expecting a different version of Byte Buddy which is based on a different version of ASM. You can find detailed information on maintaining a dependency on Byte Buddy on the front page.

The ASM library comes with an excellent documentation of Java byte code and on the use of the library. Therefore, we want to refer you to this documentation in case that you want to learn in detail about Java byte code and ASM's API. Instead, we are only going to provide a brief introduction to the JVM's execution model and Byte Buddy's adaption of ASM's API.

Any Java class file is constituted by several segments. The core segments can be categorized roughly as follows:

  • Base data: A class file references the class's name as well as the name of its superclass and its implemented interfaces. Additionally, a class file contains different metadata, as for example the class's Java version number, its annotations or the name of the source file the compiler processed for creating the class.
  • Constant pool: A class's constant pool is a collection of values that are referenced by a member or an annotation of this class. Among those values, the constant pool stores, for example, primitive values and strings that are created by some literal expression in the class's source code. Furthermore, the constant pool stores the names of all types and methods that are used within the class.
  • Field list: A Java class file contains a list of all fields that are declared in this class. Additionally to the type, name and the modifiers of a field, the class file stores the annotations of each field.
  • Method list: Similar to the list of fields, a Java class file contains a list of all declared methods. Other than fields, non-abstract methods are additionally described by an array of byte-encoded instructions that describe the method body. These instructions represent the so-called Java byte code.

Fortunately, the ASM library takes full responsibility of establishing an applicable constant pool when creating a class. With this, the only non-trivial element remains the description of a method's implementation which is represented by an array of execution instructions, each one encoded as a single byte. These instructions are processed by a virtual stack machine on the method's invocation. For a simple example, let us consider a method that calculates and returns the sum of the two primitive integers 10 and 50. This method's Java byte code would look as follows:

LDC     10  // stack contains 10
LDC     50  // stack contains 10, 50
IADD        // stack contains 60
IRETURN     // stack is empty

The above mnemonic of an array of Java byte code starts off by pushing both numbers onto the stack by using the LDC instruction. Note how this execution order differs from the order that is expressed in Java source code where the addition would be written as the infix notation 10 + 50. However, the latter order cannot be processed by a stack machine where any instruction like + can only access the uppermost values that are currently found on the stack. This addition is expressed by IADD, which consumes the two upmost stack values that are expected to be primitive integers. In the process, it adds these two values and pushes the result back onto the top of the stack. Finally, the IRETURN statement consumes this calculation result and returns it from the method, leaving us with an empty stack.

We already mentioned that any primitive value that is referenced in a method is stored in the class's constant pool. This is also true for the numbers 50 and 10 which are referenced in the above method. Any value in the constant pool is assigned an index with the length of two bytes. Let us assume that the numbers 10 and 50 were stored at the indexes 1 and 2. Together with the byte values of the above mnemonic which are 0x12 for LDC, 0x60 for IADD and 0xAC for IRETURN, we now know how express the above method as raw byte instructions:

12 00 01
12 00 02
60
AC

For a compiled class, this exact byte sequence could be found in the class file. However, this description does not yet suffice to fully define a method's implementation. In order to accelerate a Java application's runtime execution, each method is required to inform the Java virtual machine about the required size for its execution stack. For the above method which comes without branches, this is rather easy to determine as we already saw that there will be at most two values on the stack. However, for more complex methods, providing this information can easily become a complex task. And to make things worse, stack values can be of different size. Both long and double values consume two slots while any other value consumes one. As if this wasn't enough, the Java virtual machine also requires information about the size of all local variables within a method's body. All such variables in a method are stored in an array which also includes any method parameter and the this reference for non-static methods. Again, long and double values consume two slots in this array.

Evidently, keeping track of all this information makes the manual assembly of Java byte code tedious and error-prone which is why Byte Buddy provides a simplifying abstraction. Within Byte Buddy, any stack instruction is contained by an implementation of the StackManipulation interface. Any implementation of a stack manipulation combines an instruction to alter a given stack and information on the size impact of this instruction. Any number of such instructions can then easily be conflated to a common instruction. To demonstrate this, let us first implement a StackManipulation for the IADD instruction:

enum IntegerSum implements StackManipulation {

  INSTANCE; // singleton

  @Override
  public boolean isValid() {
    return true;
  }

  @Override
  public Size apply(MethodVisitor methodVisitor,
                    Implementation.Context implementationContext) {
    methodVisitor.visitInsn(Opcodes.IADD);
    return new Size(-1, 0);
  }
}

From the above apply method, we learn that this stack manipulation executes the IADD instruction by invoking the related method on ASM's method visitor. Furthermore, the method expresses that the instruction reduces the current stack Size by one slot. The second argument of the created Size instance is 0, which expresses that this instruction does not require a specific minimal stack size for calculating interim results. Furthermore, any StackManipulation can express to be invalid. This behavior can be used for more complex stack manipulation, such as object assignments which might break a type constraint. We will look at an example of an invalid stack manipulation later in this section. Finally, note that we describe the stack manipulation as a singleton enumeration. Using such immutable, functional descriptions of stack manipulations proved to be a good practice for Byte Buddy's internal implementation and we can only recommend you to follow the same approach.

By combining the above IntegerSum with the predefined IntegerConstant and the MethodReturn stack manipulations, we can now implement a method. Within Byte Buddy, a method implementation is contained by a ByteCodeAppender which we implement as follows:

enum SumMethod implements ByteCodeAppender {

  INSTANCE; // singleton

  @Override
  public Size apply(MethodVisitor methodVisitor,
                    Implementation.Context implementationContext,
                    MethodDescription instrumentedMethod) {
    if (!instrumentedMethod.getReturnType().asErasure().represents(int.class)) {
      throw new IllegalArgumentException(instrumentedMethod + " must return int");
    }
    StackManipulation.Size operandStackSize = new StackManipulation.Compound(
      IntegerConstant.forValue(10),
      IntegerConstant.forValue(50),
      IntegerSum.INSTANCE,
      MethodReturn.INTEGER
    ).apply(methodVisitor, implementationContext);
    return new Size(operandStackSize.getMaximalSize(),
                    instrumentedMethod.getStackSize());
  }
}

Again, the custom ByteCodeAppender is implemented as a singleton enumeration.

Before implementing the desired method, we first validate that the instrumented method really returns a primitive integer. Otherwise, the created class would be rejected by the JVM's validator. Then we load the two numbers 10 and 50 onto the execution stack, apply the summation of these values and return the calculation result. By wrapping all these instructions with a compound stack manipulation, we can conclusively retrieve the aggregated stack size that is required to perform this chain of stack manipulations. Finally, we return the overall size requirements of this method. The first argument of the returned ByteCodeAppender.Size reflects the required size for the execution stack, which we just mentioned to be contained by the StackManipulation.Size. Additionally, the second argument reflects the required size for the local variable array. Here it simply resembles the required size for the method's parameters and a possible this reference, since we did not define any local variables of our own.

With this implementation of our summation method, we are now ready to write a custom Implementation for this method which we can provide to Byte Buddy's domain specific language:

enum SumImplementation implements Implementation {

  INSTANCE; // singleton

  @Override
  public InstrumentedType prepare(InstrumentedType instrumentedType) {
    return instrumentedType;
  }

  @Override
  public ByteCodeAppender appender(Target implementationTarget) {
    return SumMethod.INSTANCE;
  }
}

Any Implementation is queried in two stages. First, an implementation gets the chance to alter the created class by adding additional fields or methods in the prepare method. Furthermore, the preparation allows an implementation to register a TypeInitializer which we learned about in a previous section. If no such preparations are required, it suffices to return the unaltered InstrumentedType which is provided as the argument. Note that an Implementation should not normally return an individual instance of an instrumented type, but call the instrumented type's appender methods which are all prefixed by with. After any Implementation for a particular class creation is prepared, the appender method is invoked for retrieving a ByteCodeAppender. This appender is then queried for any method that was selected for interception by the given implementation and also for any method that was registered during the implementation's invocation of the prepare method.

Note that Byte Buddy only invokes each Implementation's prepare and appender methods a single time, during the creation process of any class. This is guaranteed, no matter how many times an implementation is registered for use in a class's creation. This way, an Implementation can avoid to verify if a field or method is already defined. In the process, Byte Buddy compares Implementations instances by their hashCode and equals methods. In general, any class that is used by Byte Buddy should provide meaningful implementations of these methods. The fact that enumerations come with such implementations per definition is another good reason for their use.

With all this, let us see the SumImplementation in action:

abstract class SumExample {
  public abstract int calculate();
}

new ByteBuddy()
  .subclass(SumExample.class)
    .method(named("calculate"))
    .intercept(SumImplementation.INSTANCE)
  .make()

Congratulations! You just extended Byte Buddy to implement a custom method that computes and returns the sum of 10 and 50. Of course, this example implementation is not of much practical use. However, more complex implementations can be implemented easily on top of this infrastructure. After all, if you feel that you created something handy, please consider to contribute your implementation. We are looking forward to hearing from you!

Jump instructions

Before we move on to customizing some other components of Byte Buddy, we should briefly discuss the use of jump instructions and the matter of the so-called Java stack frames. Since Java 6, any jump instruction, which are used to implement statements such as if or while, require some additional information in order to accelerate the JVM's verification process. This additional information is called a stack map frame. A stack map frame contains information about all values that are found on the execution stack at any target of a jump instruction. By providing this information, the JVM's verifier saves some work, which is now however left to us.

For more complex jump instructions, providing correct stack map frames is a rather difficult task and many code generation frameworks have quite some trouble to always create correct stack map frames. So, how do we deal with this matter? As a matter of fact, we simply don't. It is Byte Buddy's philosophy that code generation should only be used as the glue between a type hierarchy that is unknown at compile time and custom code that needs to be injected into these types. The actual code that is generated should therefore remain as confined as possible.

Wherever possible, conditional statements should rather be implemented and compiled in a JVM language of your choice and then be bound to a given method by using a minimalistic implementation. A nice side effect of this approach is that Byte Buddy's users can work with normal Java code and use their accustomed tools like debuggers or IDE code navigators. None of this would be possible with generated code which does not have a source code representation. However, if you really need to create byte code with jump instructions, make sure to add the correct stack map frames using ASM, since Byte Buddy will not automatically include them for you.

Creating a custom assigner

In a previous section, we discussed that Byte Buddy's built in Implementations rely on an Assigner in order to assign values to variables. In this process, an Assigner is able to apply a transformation of one value to another, by emitting an appropriate StackManipulation. Doing so, Byte Buddy's built-in assigners provide, for example, auto-boxing of primitive values and their wrapper types. In the most trivial case, a value is assignable to a variable as is. In some cases, an assignment may however not be possible at all, which can be expressed by returning an invalid StackManipulation from an assigner. A canonical implementation of an invalid assignment is provided by Byte Buddy's IllegalStackManipulation class.

To demonstrate the use of a custom assigner, we are now going to implement an Assigner that only assigns values to String-typed variables by calling the toString method on any value it receives:

enum ToStringAssigner implements Assigner {

  INSTANCE; // singleton

  @Override
  public StackManipulation assign(TypeDescription.Generic source,
                                  TypeDescription.Generic target,
                                  Assigner.Typing typing) {
    if (!source.isPrimitive() && target.represents(String.class)) {
      MethodDescription toStringMethod = new TypeDescription.ForLoadedType(Object.class)
        .getDeclaredMethods()
        .filter(named("toString"))
        .getOnly();
      return MethodInvocation.invoke(toStringMethod).virtual(sourceType.asErasure());
    }

    return StackManipulation.Illegal.INSTANCE;
  }
}

The above implementation first validates that the input value is not of a primitive type and that the target variable type is of a String type. If these conditions are not fulfilled, the Assigner emits an IllegalStackManipulation to render the attempted assignment invalid. Otherwise, we identify the Object type's toString method by its name. We then use Byte Buddy's MethodInvocation to create a StackManipulation that calls this method virtually on the source type. Finally, we can integrate this custom Assigner with, for example, Byte Buddy's FixedValue implementation as follows:

new ByteBuddy()
  .subclass(Object.class)
  .method(named("toString"))
    .intercept(FixedValue.value(42)
      .withAssigner(new PrimitiveTypeAwareAssigner(ToStringAssigner.INSTANCE),
                    Assigner.Typing.STATIC))
  .make()

When the toString method is called on an instance of the above type, it will return the string value 42. This is only possible by using our custom assigner which converts the Integer type to a String by invoking the toString method. Note that we additionally wrapped the custom assigner with the built-in PrimitiveTypeAwareAssigner which performs an auto-boxing of the provided primitive int to its wrapper type before delegating the assignment of this wrapped primitive value to its inner assigner. Other built-in assigners are the VoidAwareAssigner and the ReferenceTypeAwareAssigner. Always remember to implement meaningful hashCode and equals methods for your custom assigners since those methods are normally called from their counterparts in the Implementation that is making use of a given assigner. Again, by implementing an assigner as a singleton enumeration, we avoid doing this manually.

Creating a custom parameter binder

We already mentioned in a previous section that it is possible to extend the MethodDelegation implementation to process user-defined annotations. For this purpose, we need to provide a custom ParameterBinder which knows how to handle a given annotation. As an example, we want to define an annotation with the purpose to simply inject a fixed string into the annotated parameter. First, we define such a StringValue annotation:

@Retention(RetentionPolicy.RUNTIME)
@interface StringValue {
  String value();
}

We need to make sure that the annotation is visible at runtime by setting the appropriate RuntimePolicy. Otherwise, the annotation is not retained at runtime and Byte Buddy does not have a chance to discover it. Doing so, the above value property contains the string which is assigned to the annotated parameter as a value.

With our custom annotation, we need to create a corresponding ParameterBinder which is able to create a StackManipulation which expresses the binding for this parameter. This parameter binder is invoked, by the MethodDelegation, each time its corresponding annotation is discovered on a parameter. Implementing a custom parameter binder for our example annotation is straight forward:

enum StringValueBinder
    implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> {

    INSTANCE; // singleton

    @Override
    public Class<StringValue> getHandledType() {
        return StringValue.class;
    }

    @Override
    public MethodDelegationBinder.ParameterBinding<?> bind(
        AnnotationDescription.Loadable<StringValue> annotation,
        MethodDescription source,
        ParameterDescription target,
        Implementation.Target implementationTarget,
        Assigner assigner,
        Assigner.Typing typing)
    {
        if (!target.getType().asErasure().represents(String.class)) {
            throw new IllegalStateException(target + " makes illegal use of @StringValue");
        }
        StackManipulation constant = new TextConstant(annotation.load().value());
        return new MethodDelegationBinder.ParameterBinding.Anonymous(constant);
    }
}

Initially, the parameter binder makes sure that the target parameter is actually a String type. If this is not the case, we will throw an exception to inform the annotation's user of his illegal placing of this annotation. Otherwise, we simply create a TextConstant which represents the loading of a constant pool string onto the execution stack. This StackManipulation is then wrapped as an anonymous ParameterBinding, which is finally returned from the method. Alternatively, you could have provided either a Unique or an Illegal parameter binding. A unique binding is identified by any object which allows to retrieve this binding from an AmbiguityResolver. In a later step, such a resolver is able to look up if a parameter binding was registered with some unique identifier, then it can decide if this binding is superior to another successfully bound method. With an illegal binding, one can instruct Byte Buddy that a specific pair of a source and a target method is incompatible and cannot be bound together.

This already is all the information that is required for using a custom annotation with a MethodDelegation implementation. After receiving a ParameterBinding, it makes sure that its value is bound to the correct parameter, or it will discard the current pair of a source and target method as unbindable. Furthermore, it will allow AmbiguityResolvers to look up unique bindings. Finally, let us put this custom annotation in action:

class ToStringInterceptor {
  public static String makeString(@StringValue("Hello!") String value) {
    return value;
  }
}

new ByteBuddy()
  .subclass(Object.class)
  .method(named("toString"))
    .intercept(MethodDelegation.withDefaultConfiguration()
      .withBinders(StringValueBinder.INSTANCE)
      .to(ToStringInterceptor.class))
  .make()

Note that by specifying the StringValueBinder as the only parameter binder, we replace all defaults. Alternatively, we could have appended the parameter binder to those that are already registered. With only one possible target method in the ToStringInterceptor, the dynamic class's intercepted toString method is bound to the makeString invocation. When the target method is invoked, Byte Buddy assigns the annotation's string value as the target method's only parameter.