[Translation] Byte Buddy Tutorial

https://bytebuddy.net/#/tutorial google translated the tutorial of bytebuddy

Why runtime code generation?

The Java language comes with a relatively strict type system. In Java, all variables and objects must be of a particular type, and you will always get an error if you try to assign an incompatible type. These errors are usually issued by the Java compiler, or at least by the Java runtime, when you cast a type incorrectly. Such rigorous typing is often desirable, for example when writing business applications. Business domains can usually be described in an explicit way that any domain entry represents its own type. This allows you to use Java to build very readable and robust applications where mistakes are detected close to the source. In particular, Java's type system is responsible for Java's popularity in enterprise programming.

However, by enforcing that strict type system, Java imposes restrictions that limit the range of languages in other domains. For example, when writing a generic library that is to be used by other Java applications, we do not know these types to us when our library is compiled, so your application Cannot reference the type defined in. The Java Class Library comes with a reflection API to call methods or access fields in your unknown code. You can use the reflection API to introspect unknown types, call methods, and access fields. Unfortunately, using the Reflection API has two major drawbacks.

This is where runtime code generation can help us. This allows you to emulate some features that are normally accessible only when programming in a dynamic language, without breaking Java's static type checking. In this way, you can maximize the best of both worlds and further improve runtime performance. To better understand this issue, let's look at an example of implementing the method-level security library mentioned above.

Writing a security library

Business applications can grow and it can be difficult to get an overview of the call stack within your application. This can be a problem if you have important methods in your application that should only be called under certain conditions. Imagine a business application that implements a reset feature that allows you to delete everything from your application's database.

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

Such resets should, of course, only be performed by the administrator and should never be performed by the average user of our application. By analyzing our source code, of course, we can be sure that this does not happen. But we can expect our applications to grow and change in the future. Therefore, you need to implement a stricter security model in which method calls are protected by explicit checks on the current user of the application. In general, use a security framework to prevent this method from being called by anyone other than the administrator.

For this purpose, suppose you are using a security framework with a public API, like this:

@Retention(RetentionPolicy.RUNTIME)
@interface Secured {
  String user();
}
 
class UserHolder {
  static String user;
}
 
interface Framework {
  <T> T secure(Class<T> type);
}

This framework requires you to use the Secured annotation to mark methods that are accessible only to specific users. ʻUserHolderis used to globally define which user is currently logged in to the application. TheFrameworkinterface allows you to create a protected instance by calling the default constructor of a given type. Of course, this framework is very simple, but in principle, this is how a security framework, such as the popular Spring Security, works. A feature of this security framework is that it retains the type of user. By contracting with ourFramework interface, we promise to return an instance of any type T` that the user receives. This allows the user to interact with his own type as if the security framework did not exist. In a test environment, users can even create unprotected instances of their type and use these instances instead of protected instances. You will agree that this is really useful! Such frameworks are known to interact with POJOs (plain Java objects). This is a term coined to describe a non-intrusive framework that does not impose its own type on the user.

For now, we know that the type passed to Framework is only T = Service, and that the deleteEverything method is annotated with@Secured ("ADMIN"). Imagine. In this way, you can easily implement a protected 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");
    }
  }
}

You can use this additional class to 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 very useful. The secure method signature suggested that this method could provide any type of security, but in reality it throws an exception if something other than a known service occurs. This will also require our security library to know about this particular service type when the library is compiled. Obviously, this is not a viable solution for implementing the framework. So how can we solve this problem? This is a code generation library tutorial, so you can guess the answer. If necessary, create a subclass at run time when the Service class becomes visible to the security framework by calling the secure method. In code generation, you can take a given type, subclass it at runtime, and override the method you want to protect. In this case, it overrides all the methods annotated with @ Secured and reads the required user from the annotation's ʻuser` property. Many popular Java frameworks are implemented in a similar way.

General information

Use code generation carefully before learning all about code generation and Byte Buddy. Java types are quite special to the JVM and are often not garbage collected. Therefore, do not overuse code generation and use generated code only if the generated code is the only solution to solve the problem. However, if you need to extend an unknown type, as in the previous example, code generation is probably the 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 generating code on the JVM. However, Byte Buddy believes he knows some tricks that aren't applicable in other frameworks. The overall purpose of Byte Buddy is to work declaratively by focusing on both its domain-specific language and the use of annotations. Other code generation libraries for the JVM we know will not work this way. Nevertheless, you may want to look at some other frameworks for code generation to find out which one works best for you. In particular, the following libraries are widespread in the Java field.

Evaluate the framework for yourself, but we believe Byte Buddy offers the features and conveniences you would otherwise find in vain. Byte Buddy comes with an expressive domain-specific language that allows you to create highly custom runtime classes by writing plain Java code or using strong typing in your own code. doing. At the same time, Byte Buddy is highly customizable and does not limit the functionality that comes out of the box. If desired, you can also define custom bytecode for the implemented method. But you can do a lot without digging into the framework without knowing what the bytecode is and how it works. For example, did you see Hello World? An example? Byte Buddy is that easy to use.

Of course, comfortable APIs aren't the only things to consider when choosing a code generation library. For many applications, the run-time characteristics of the generated code are likely to determine the best choice. Also, the execution time for creating a dynamic class can be an issue beyond the execution time of the generated code itself. We claim to be the fastest It's as easy as it's hard to provide a valid metric for library speed. Nevertheless, I would like to provide such a metric as a basic orientation. However, keep in mind that these results do not necessarily translate into specific use cases where individual metrics need to be implemented.

Before we talk about metrics, let's take a look at the raw data. The following table shows the average execution time in nanoseconds for operations whose standard deviation is enclosed in curly braces.

baseline Byte Buddy cglib Javassist Java proxy
trivial class creation 0.003 (0.001) 142.772 (1.390) 515.174 (26.753) 193.733 (4.430) 70.712 (0.645)
interface implementation 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 0.002 (0.001) 0.002 (0.001) 0.003 (0.001) 0.011 (0.001) 0.008 (0.001)
class extension 0.004 (0.001) 885.983 (7.901) 5'408.329 (52.437) 1'632.730 (52.737) 683.478 (6.735)
super method invocation 0.004 (0.001) 0.004 (0.001) 0.004 (0.001) 0.021 (0.001) 0.025 (0.001) -

Like static compilers, code generation libraries face trade-offs between fast code generation and fast code generation. When choosing between these conflicting goals, Byte Buddy's main focus is on generating code with minimal execution time. Creating and manipulating types is usually not a common procedure in any program and does not significantly affect long-running applications. In particular, loading classes and instrumenting classes are the most time-consuming and unavoidable steps when executing such code.

The first benchmark in the table above measures the execution time of the library for subclassing ʻObject` without implementing or overriding the method. This gives the impression of the library's general overhead in code generation. In this benchmark, Java proxies perform better than other libraries, with optimizations that are only possible assuming that the interface is always extended. Byte Buddy also checks generic types and annotation classes and triggers an additional runtime. This performance overhead is also found in other benchmarks for creating classes. Benchmark (2a) shows the runtime measured to create (and load) a class that implements a single interface with 18 methods, and (2b) shows the methods generated for this class. Indicates the execution time. Similarly, (3a) shows a benchmark for extending a class with the same 18 methods implemented. Byte Buddy offers two benchmarks. This is due to possible optimizations for interceptors that always execute supermethods. At the expense of time during class creation, the execution time of a class created with Byte Buddy usually reaches the baseline. In other words, instrumentation does not incur any overhead. Keep in mind that Byte Buddy is better than other code generation libraries while creating classes when metadata processing is disabled. However, since the execution time of code generation is very short compared to the total execution time of the program, such an opt-out is not available because it provides little performance at the expense of complicating the library code.

Finally, our metric measures the performance of Java code previously optimized by the JVM's Just-in-Time Compiler (http://en.wikipedia.org/wiki/Just-in-time_compilation). Please note in particular. If the code runs only occasionally, the performance will be worse than the metrics above suggest. However, in this case, the performance of the code is less important. The code for this metric is distributed with Byte Buddy, so you can run these metric on your computer and adjust the above numbers according to the processing power of your machine. For this reason, rather than interpreting the numbers above absolutely, consider them as a relative measure comparing different libraries. As you develop your Byte Buddy further, you should monitor these metrics to avoid performance degradation as you add new features.

In the next tutorial, you'll gradually explore the features of Byte Buddy. Start with its more general features that most users are most likely to use. After that, we'll explore more and more advanced topics and give a brief introduction to Java bytecode and class file formats. And don't be discouraged if you fast forward to the material that follows. You can do almost anything without having to understand the details of the JVM using Byte Buddy's standard API. Read on to learn about the standard API.

Creating a class

The types created by Byte Buddy are issued by an instance of the ByteBuddy class. Just call new ByteBuddy () to create a new instance and you're ready to go. Hopefully you are using a development environment where you get suggestions on the methods you can call on a given object. In this way, you can use the IDE to guide the process while avoiding manually searching the class API in the Byte Buddy javadoc. As mentioned earlier, Byte Buddy provides a domain-specific language that is intended to be as human-readable as possible. Therefore, IDE tips will almost always allow you to go in the right direction. But that's enough, let's create the first class when we run our Java program.

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

Obviously, the above code example creates a new class that extends the ʻObject type. This dynamically created type is equivalent to a Java class that simply extends ʻObject without explicitly implementing a method, field, or constructor. You may have noticed that you didn't even name the dynamically generated types, which is usually needed when defining Java classes. Of course, you can easily name your type explicitly:

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

But what happens without an explicit name? Byte Buddy lives on the convention over configuration (http://en.wikipedia.org/wiki/Convention_over_configuration) and provides convenient default settings. For type names, the default Byte Buddy setting provides a NamingStrategy that randomly creates a class name based on the dynamic type superclass name. In addition, the name is defined to be in the same package as the superclass, so direct superclass package private methods are always recognized as dynamic types. For example, if you subclassed the type ʻexample.Foo, the generated name would be ʻexample.Foo $$ ByteBuddy $$ 1376491271. Here, the numeric sequence is random. There is an exception to this rule when subclassing a type from a java.lang package that has a type such as ʻObject. The Java security model does not allow custom types in this namespace. Therefore, the default naming scheme prefixes such type names with net.bytebuddy.renamed`.

This default behavior may not be convenient for you. Also, thanks to the conventions for configuration principles, you can change the default behavior at any time as needed. This is where the ByteBuddy class was introduced. Create the default settings by creating a new ByteBuddy () instance. You can customize it to your individual needs by calling methods with this setting. Let's try this:

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

In the code example above, we created a new setting where the type naming strategy is different from the default setting. The anonymous class is implemented to simply concatenate the string ʻi.love.ByteBuddy with the simple name of the base class. Therefore, when subclassing the ʻObject type, the dynamic type will be named ʻi.love.ByteBuddy.Object. Be careful when creating your own naming scheme. The Java virtual machine uses names to distinguish between types, so we want to avoid naming conflicts. If you need to customize the naming behavior, you can use the NamingStrategy.SuffixingRandom` built into Byte Buddy to customize it to include prefixes that are more meaningful to your application than the defaults.

Domain specific language and immutability

After seeing Byte Buddy's domain-specific language in action, we need to take a quick look at how this language is implemented. One detail you need to know about the implementation is that the language is built around Immutable Objects (http://en.wikipedia.org/wiki/Immutable_object). In fact, almost every class that exists in the Byte Buddy namespace is immutable, but in some cases it wasn't possible to immutable the type. We recommend that you follow this principle when implementing custom features in Byte Buddy.

As for the immutability mentioned above, be careful when configuring a ByteBuddy instance, for example. For example, you might make the following mistake:

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

The dynamic type should be generated using the (probably) defined custom naming strategy new NamingStrategy.SuffixingRandom ("suffix") . Calling the withNamingStrategy method instead of modifying the instance stored in the byteBuddy variable returns a customized ByteBuddy instance, but this is lost. As a result, dynamic types are created using the default configuration created first.

Redefining and rebasing existing classes

So far, we've shown you how to use Byte Buddy to create subclasses of existing classes. However, you can also use the same API to extend an existing class. Such enhancements are available in two different flavors.

type redefinition

When redefining a class, Byte Buddy allows you to modify an existing class by adding fields and methods or replacing existing method implementations. However, existing method implementations will be lost if replaced by another implementation. For example, if you redefine the following type

class Foo {
  String bar() { return "bar"; }
}

Because the bar method returns"qux", the information that this method originally returned"bar"is lost altogether.

type rebasing

When you rebase a class, Byte Buddy keeps all the method implementations of the rebased class. Instead of discarding the overridden method as when performing a type redefinition, Byte Buddy copies all such method implementations to a renamed private method with a compatible signature. .. In this way, the implementation is not lost and the rebased methods can continue to call the original code by calling these renamed methods. Thus, the above class Foo can be rebased as follows:

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}

The information that the bar method originally returned"bar"is stored in another method and remains accessible. When rebasing a class, Byte Buddy handles all method definitions, such as when you define a subclass. That is, when you try to call the supermethod implementation of a rebase method, the rebase method is called. But instead, it eventually flattens this virtual superclass to the rebased type shown above.

Rebasing, redefining, or subclassing is performed using the same API defined by the DynamicType.Builder interface. In this way, for example, you can define a class as a subclass and later change that definition to represent the rebased class instead. This can be achieved by simply changing one word in Byte Buddy's domain-specific language. This method applies one of the possible approaches

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

The rest of the definition process described in the rest of this tutorial is transparent. Because subclass definitions are a familiar concept to Java developers, all of the following descriptions and examples of Byte Buddy's domain-specific languages are shown by creating subclasses. However, keep in mind that all classes can be defined in the same way by redefining or redefining.

Loading a class

So far, I've just defined and created a dynamic type, but I haven't used it. The type created by Byte Buddy is represented by an instance of DynamicType.Unloaded. As the name implies, these types are not loaded into the Java Virtual Machine. Instead, the classes created by Byte Buddy are represented in binary format in the Java class file format. Thus, what you want to do with the generated type is up to you. For example, you can run Byte Buddy from a build script that generates only the classes you want to extend before deploying your Java application. For this purpose, the DynamicType.Unloaded class allows you to extract a byte array that represents a dynamic type. For convenience, this type also has a saveIn (File) method that allows you to save the class to a specific folder. You can also inject the class into an existing jar file with ʻinject (File)`.

It's easy to access the binary form of a class directly, but unfortunately loading types is more complicated. In Java, all classes are loaded using ClassLoader. An example of such a class loader is the bootstrap class loader responsible for loading the classes shipped within the Java class library. The system class loader, on the other hand, is responsible for loading the class into the Java application's classpath. Obviously, none of these existing class loaders are aware of the dynamic classes we created. To overcome this, we need to find other possibilities for loading runtime-generated classes. Byte Buddy offers solutions in a variety of approaches right out of the box.

Unfortunately, the above approach has both drawbacks.

After creating DynamicType.Unloaded, this type can be loaded using ClassLoadingStrategy. If no such strategy is provided, Byte Buddy will infer such a strategy based on the provided class loader, otherwise only for bootstrap class loaders that cannot inject types using the default reflection. Create a new class loader for. Byte Buddy offers several class loading strategies that you can use right out of the box. Each strategy follows one of the above concepts. These strategies are defined in ClassLoadingStrategy.Default. The WRAPPER strategy creates a new wrapping ClassLoader. Here, the CHILD_FIRST strategy creates a similar class loader with the child having the first semantics. Both the WRAPPER and CHILD_FIRST strategies are also available in the so-called manifest version, where the binary form of the type is preserved after the class is loaded. These alternative versions allow you to access the binary representation of the class loader's classes via the ClassLoader :: getResourceAsStream method. However, keep in mind that to do this, these class loaders need to maintain a reference to the full binary representation of the classes that consume space on the JVM's heap. Therefore, if you actually plan to access the binary format, use only the manifest version. Of course, it isn't available in the manifest version because the ʻINJECTIONstrategy works through reflection and doesn't have the potential to change the semantics of theClassLoader :: getResourceAsStream` method.

Let's actually see the loading of such a class.

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

In the above example, we created and loaded the class. As mentioned earlier, we used the WRAPPER strategy to load the appropriate class in most cases. Finally, the getLoaded method returns an instance of the Java class that represents the currently loaded dynamic class.

When loading a class, a predefined class loading strategy is executed by applying the ProtectionDomain in the current execution context. Alternatively, all default strategies provide an explicit protection domain specification by calling the withProtectionDomain method. Defining an explicit protection domain is important when using Security Manager or when working with classes defined in signed jars.

Reloading a class

The previous section described how you can use Byte Buddy to redefine or rebase an existing class. However, it is not possible to guarantee that a particular class has not yet been loaded while the Java program is running. (In addition, Byte Buddy currently only takes load classes as arguments. In future versions, it will work as well as unload classes using existing APIs) even after they have been loaded. This feature is made accessible by Byte Buddy's ClassReloadingStrategy. Let's redefine class Foo to demonstrate this strategy.

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

You can easily redefine the Foo class to be Bar using Byte Buddy. With HotSwap, this redefinition also applies to existing instances.

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 can only be accessed using so-called Java agents. Such agents can be installed by specifying them using the -javaagent parameter when starting the Java Virtual Machine. The parameter argument is the Byte Buddy agent jar that you can download from the Byte Buddy Bintray page. However, if the Java application is run from a Java virtual machine JDK installation, Byte Buddy can still load the Java agent after the application is launched with ByteBuddyAgent.installOnOpenJDK (). This is a very convenient method, as class redefinition is primarily used to implement tools and tests. Starting with Java 9, it is also possible to install the agent at runtime without installing the JDK.

One thing that may seem counterintuitive in the above example is the fact that Byte Buddy is instructed to redefine the Bar type, where the Foo type is finally redefined. The Java Virtual Machine identifies the type by name and class loader. Therefore, by renaming the Bar to Foo and applying this definition, we will eventually redefine the renamed type of Bar. Of course, it's also possible to redefine Foo directly without renaming different types.

However, using Java's HotSwap feature has one major drawback. The current implementation of HotSwap requires the redefined class to apply the same class schema before and after the class redefinition. That is, you cannot add methods or fields when reloading a class. We've already seen that Byte Buddy defines a copy of the original method of the rebase class so that class rebase doesn't work with ClassReloadingStrategy. Also, class redefinition does not work for classes that have explicit class initialization methods (static blocks within the class). This is because this initialization method also needs to be copied to the additional method. However, there are plans to extend HotSwap in the future, and Byte Buddy is ready to use this feature as soon as it works. In the meantime, Byte Buddy's HotSwap support can be used for corner cases that you find useful. Otherwise, class relocation and redefinition can be a useful feature, for example, when extending an existing class from a build script.

Working with unloaded classes

With this awareness of the limitations of Java's HotSwap functionality, you might think that the only meaningful application of rebase and redefinition instructions is at build time. By applying build-time operations, you can assert that the processed class will not be loaded before the first class load. This is simply because this class loading is done in another instance of the JVM. Byte Buddy works the same for classes that haven't been loaded yet. To this end, Byte Buddy abstracts Java's reflection API so that the Class instance is represented internally by, for example, an instance of TypeDescription. In fact, Byte Buddy only knows how to handle the classes provided by the adapter that implements the TypeDescription interface. The big advantage to this abstraction is that the information about the class does not have to be provided by ClassLoader, it can be provided by any other source.

Byte Buddy provides a standard way to get the TypeDescription of a class using TypePool. Of course, a default implementation of such a pool is also provided. This TypePool.Default implementation parses the binary form of the class and represents it as the required TypeDescription. Like ClassLoader, it also manages a cache of expressible classes. This is also customizable. It also usually gets the binary form of the class from ClassLoader, but does not tell it to load this class.

The Java Virtual Machine only loads the class on first use. As a result, you can safely redefine a class, for example:

package foo;
class Bar { }

Run it at program startup before running any other code.

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

You can prevent the JVM from loading built-in classes by explicitly loading the redefined class before it is first used in an assertion statement. In this way, the redefined definition of foo.Bar is loaded and used throughout the application run time. However, when you use TypePool to provide a description, you do not reference the class in class literals. If you used a class literal for foo.Bar, the redefinition attempt would be invalid because the JVM was loading this class before the changes were made to redefine it. Also, when dealing with unloaded classes, you need to specify ClassFileLocator where you can find the class files for the class. The above example simply creates a class file locator that scans the classpath for such files in a running application.

Creating Java agents

As applications grow and become more modular, applying such transformations at specific program points is, of course, a cumbersome constraint to implement. And there is a better way to apply such class redefinition on demand. Use the Java Agent (https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html) to directly view class loading activities that take place within your Java application. It is possible to intercept. The Java agent is implemented as a simple jar file with the entry points specified in the manifest file for this jar file, as described under Linked Resources. With Byte Buddy, implementing such an agent is easy with ʻAgentBuilder. Assuming you have previously defined a simple annotation named ToString, implement the toStringmethod for all annotated classes by simply implementing the agent'spremain` method as follows: Is easy.

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 typeDescription,
                                              ClassLoader classloader) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }
    }).installOn(instrumentation);
  }
}

As a result of applying the above ʻAgentBuilder.Transformer, all toStringmethods of the annotated class now return transformed. Byte Buddy'sDynamicType.Builder` will be discussed in a future section, but don't worry about this class for now. The above code is, of course, a trivial and meaningless application. Proper use of this concept makes it a powerful tool for easily implementing aspect-oriented programming.

It is also possible to instrument the classes loaded by the bootstrap class loader when using the agent. However, this requires some preparation. First of all, the bootstrap classloader is represented by a null value, so it is not possible to load a class into this classloader using reflection. However, this may be required to load the helper class into the measurement class class loader to support class implementation. To load the 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, these classes must be saved to disk. Folders for these classes can be specified using the ʻenableBootstrapInjection command, which also gets an instance of the ʻInstrumentation interface to add the class. All user classes used by the instrumentation class must also be placed in a possible bootstrap search path using the instrumentation interface.

Loading classes in Android applications

Android uses a different class file format, using a dex file that is not in the Java class file format layout. In addition, the ART runtime, which inherits from the Dalvik virtual machine, compiles Android applications into native machine code before they are installed on Android devices. As a result, Byte Buddy has no intermediate code representation to interpret and can no longer redefine or relocate classes unless the application is explicitly deployed with its Java source. However, Byte Buddy can define new classes using DexClassLoader and the built-in dex compiler. To this end, Byte Buddy provides a byte-buddy-android module that includes ʻAndroidClassLoadingStrategy` that allows you to load dynamically created classes from within your Android application. For it to work, you need a folder to write temporary files and compiled class files. This folder is prohibited by Android Security Manager and should not be shared between different applications.

Working with generic types

Byte Buddy handles generic types as defined by the Java programming language. Generic types are not considered by the Java runtime, which only handles the elimination of generic types. However, generic types are still embedded in any Java class file and are exposed by the Java reflection API. Therefore, it makes sense to include generic information in the generated classes, as generic type information can affect the behavior of other libraries and frameworks. Embedding generic type information is also important when the class is persistent and is treated as a library by the Java compiler.

When subclassing a class, implementing an interface, or declaring a field or method, Byte Buddy accepts Java Type instead of the erased Class. Generic types can also be explicitly defined using TypeDescription.Generic.Builder. One important difference between Java generic types for type elimination is the contextual implications of type variables. A type variable with a particular name defined by one type does not necessarily represent the same type if another type declares the same type variable with the same name. Therefore, Byte Buddy rebinds all generic types that represent type variables in the context of the generated type or method when the Type instance is passed to the library.

Byte Buddy transparently inserts Bridge Methods (https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html) when the type is created. The bridge method is resolved by the property of the ByteBuddy instance, MethodGraph.Compiler. The default method The graph compiler behaves like a Java compiler and handles generic type information in class files. For languages other than Java, a differential graph compiler may be a good choice.

Fields and methods

Most of the types created in the previous section do not define fields or methods. However, by subclassing ʻObject, the created class inherits the methods defined by its superclass. Check this Java trivia and call the toString` method on an instance of the dynamic type. You can get an instance by calling the constructor of the class you created 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 # toStringmethod returns a concatenation of the fully qualified class name of the instance and the hexadecimal representation of the instance's hash code. And in fact, calling thetoString method on the created instance returns something like ʻexample.Type@340d1fa5.

Of course, we're not doing it here. The main motivation for creating dynamic classes is the ability to define new logic. Let's start with a simple one to show how this is done. Overrides the toString method and returnsHello 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 I added to the code contains two instructions in Byte Buddy's domain-specific language. The first instruction is a method that allows you to select as many methods as you want to override. This selection is applied by passing ʻElementMatcher, which acts as a predicate to determine whether to override each overridable method. Byte Buddy comes with a number of predefined method matchers, collected in the ʻElementMatchers class. Normally, you would import this class statically to make the resulting code more natural to read. Such a static import was also envisioned in the above example using a named method matcher that selects the method by the exact name. Predefined method matchers are configurable. In this way, the method choices can be explained in more detail as follows:

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

This latter method matcher only matches this particular method because it describes the toString method with a full Java signature. But in the given context, we know that there is no other method named toString with a different signature so that our original method matcher is sufficient.

After selecting the toString method, the second instruction intercept determines the implementation that overrides all the methods of the specified selection. To know how to implement a method, this instruction requires a single argument of the implementation type. The above example utilizes the FixedValue implementation that ships with Byte Buddy. As the name of this class suggests, the implementation always implements a method that returns a specific value. A little later in this section, we'll discuss the implementation of FixedValue in detail. Now let's take a closer look at method selection.

So far, we have intercepted only one method. In a real application, things can get more complicated and you may want to apply different rules to override different methods. Let's 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 have defined three different rules for overriding methods. Examining the code, we can see that the first rule involves the methods defined by Foo, that is, all three methods in the sample class. The second rule matches both methods named foo, which is a subset of the previous selection. And the last rule only matches the foo (Object) method. This is a further reduction of the former choice. But if this choice is duplicated, how does Byte Buddy determine which rule applies to which method?

Byte Buddy organizes rules for overriding methods in a stack format. That is, whenever you register a new rule to override a method, it will be pushed to the top of this stack and will always be applied first until a new rule is added. In the example above, this means:

For this organization, you should always register a more specific method matcher last. Otherwise, the less specific method matchers registered later may not apply the previously defined rules. Note that you can define the ʻignoreMethod property in the ByteBuddy` setting. A method that matches well with this method matcher will never be overwritten. By default, Byte Buddy does not override synthetic methods.

In some scenarios, you may want to define a supertype method or a new method that does not override the interface. This is also possible with Byte Buddy. For this purpose, you can call defineMethod wherever you can define a signature. Once you have defined the method, you will be asked to provide an implementation, similar to the method identified by the method matcher. Method matchers registered after a method is defined may take precedence over this implementation due to the stacking principles described earlier.

defineField allows Byte Buddy to define a particular type of field. In Java, fields are not overridden, they are just Shadowed (http://en.wikipedia.org/wiki/Variable_shadowing). Therefore, field matching etc. cannot be used.

With this knowledge of how to choose methods, you are ready to learn how you can implement these methods. For this purpose, let's take a look at the predefined ʻImplementation` implementation that ships with Byte Buddy. The definition of a custom implementation is described in its own section, but is only intended for users who need to implement very custom methods.

A closer look at fixed values

The implementation of FixedValue is already running. As the name implies, the method implemented by FixedValue simply returns the provided object. Classes can memorize such objects in two different ways.

When you implement a method with FixedValue # value (Object), Byte Buddy analyzes the type of the parameter and defines it to be stored in a dynamic class pool if possible. Otherwise, store the value in a static field. However, if the value is stored in the class pool, the instance returned by the selected method can be of a different object ID. Therefore, you can use FixedValue # reference (Object) to tell Byte Buddy to always store an object in a static field. The latter method is overloaded so that you can specify the name of the field as the second argument. Otherwise, the field name is automatically derived from the object's hash code. The exception to this behavior is the null value. The null value is never stored in the field, but is simply represented by its literal expression.

You may be wondering about type safety in this context. Obviously, you can define a method that returns an invalid value.

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

It is difficult to prevent this invalid implementation by the compiler in the Java type system. Instead, Byte Buddy throws ʻIllegalArgumentException when the type is created, enabling incorrect integer assignment to methods that return String`. Byte Buddy will do everything in its power to ensure that all types created are legitimate Java types and will throw exceptions and fail fast while creating illegal types.

The Byte Buddy allocation behavior is customizable. Again, Byte Buddy provides only legitimate defaults that mimic the Java compiler's assignment behavior. As a result, Byte Buddy allows you to assign types to any of its supertypes and also considers boxing of primitive values or unboxing of their wrapper representations. However, keep in mind that Byte Buddy does not currently fully support generic types and only considers type elimination. Therefore, Byte Buddy can cause heap pollution. Instead of using a predefined assigner, you can always implement your own assigner that allows type conversion that is not implicitly included in the Java programming language. You'll find out about such custom implementations in the last section of this tutorial. For now, I'm mentioning that you can define such a custom assigner 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. To be more flexible, Byte Buddy provides a MethodDelegation implementation. This gives you maximum freedom in responding to method calls. Method delegation defines a method of a dynamically created type in order to forward the call to another method that may be outside the dynamic type. In this way, the logic of dynamic classes can be represented using plain Java, but code generation only provides binding to other methods. Before we dive into the details, let's take a look at an example of using 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");

This example delegates a call to the Source # hello (String) method to the Target method so that the method returnsHello World!Instead of null. To this end, the MethodDelegation implementation identifies callable methods of type Target and identifies the best match between those methods. In the above example, the Target type defines only a single static method, which is useful because the method's parameters, return type, and name are the same as those ofSource # name (String). ..

In reality, deciding which method to delegate is probably more complicated. So how does Byte Buddy decide how to do it when there is a real choice? To do this, suppose the Target class is defined as:

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(); }
}

As you may have noticed, all of the above methods are called intercepts. Byte Buddy does not require the target method to have the same name as the source method. We will soon investigate this issue in detail. More importantly, if you change the definition of Target and run the previous example, you will see that thenamed (String)method is bound to ʻintercept (String). But why? Obviously, the ʻintercept (int) method cannot accept the String argument of the source method, so there is no chance of a match. However, this is not the case for the bindable ʻintercept (Object) method. To resolve this ambiguity, Byte Buddy once again mimics the Java compiler by choosing the method binding with the most specific parameter type. Remember how the Java compiler chooses bindings for overloaded methods. Since String is more specific than ʻObject, the ʻintercept (String)` class is finally selected from three choices.

With the information so far, you might think that the method binding algorithm is a fairly rigid property. But we haven't told the full story yet. So far, we've only observed another example of a convention for setting principles that can be changed if the defaults don't meet the actual requirements. In practice, the implementation of MethodDelegation works with annotations that determine which value the parameter annotation should be assigned to. However, if no annotation is found, Byte Buddy treats the parameter as if it were annotated with @Argument. This latter annotation causes Byte Buddy to assign the nth argument of the source method to the annotated target. If no annotations are explicitly added, the value of n is set to the index of the annotated parameter. According to this rule, Byte Buddy treats it as follows:

void foo(Object o1, Object o2)

As if all parameters were annotated as follows:

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

As a result, the first and second arguments 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 type is not assigned from the parameter type of the instrumented method, the interceptor method in question is discarded.

In addition to the @Argument annotation, there are some other predefined annotations that you can use with MethodDelegation.

In addition to using predefined annotations, Byte Buddy allows you to define your own annotations by registering one or more ParameterBinders. You'll find out about such customizations in the last section of this tutorial.

In addition to the four annotations described so far, there are two other predefined annotations that allow access to the super-implementation of dynamic methods. In this way, dynamic types can add an aspect (http://en.wikipedia.org/wiki/Aspect-oriented_programming) to the class, such as logging method calls. You can also use the @SuperCall annotation to make a super-implementation call to a method from outside a dynamic class, as shown 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, the super method is called from its calling method by injecting an instance of Callable into LoggerInterceptor, which calls the implementation of the first non-overriding MemoryDatabase # load (String). Is clear. This helper class is called ʻAuxiliaryTypein Byte Buddy terminology. Auxiliary types are created on demand by Byte Buddy and can be accessed directly from theDynamicTypeinterface after the class is created. Because of these auxiliary types, manually creating one dynamic type can create several additional types that help implement the original class. Finally, note that the@SuperCall annotation can also be used with the Runnable` type, which removes the return value of the original method.

You may still be wondering how this auxiliary type can call supermethods of other types, which is normally prohibited in Java. Upon closer inspection, this behavior is very common and is similar to the compiled code that is generated when the following Java source code snippet is 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));
  }
}

However, you may need to call the supermethod with different arguments than those assigned in the original call to the method. This is also possible with Byte Buddy by using the @Super annotation. This annotation triggers the creation of another ʻAuxiliaryType` that extends the dynamic type superclass or interface in question. As before, the auxiliary type overrides all methods to call the dynamic super implementation. In this way, you can implement the logger interceptor example from the previous example to modify the actual call.

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");
    }
  }
}

The instance assigned to the parameter annotated with @Super has a different ID than the actual instance of the dynamic type. Therefore, the instance fields accessible by the parameters do not reflect the fields of the actual instance. In addition, the non-overridable methods of auxiliary instances retain their original implementation, which can cause absurd behavior when they are called, rather than delegating their calls. Finally, if the parameter annotated with @Super does not represent the supertype of the associated dynamic type, then the method is not considered a binding target for that method.

The @Super annotation allows the use of arbitrary types, so you may need to provide information on how this type can be configured. By default, Byte Buddy tries to use the class's default constructor. This always works for interfaces that implicitly extend the ʻObjecttype. However, when extending a dynamic superclass, this class may not provide a default constructor. In such cases, or if you need to use a specific constructor to create such ancillary types, use the@Super annotation to set the parameter type as the annotation's constructorParametersproperty. By doing so, you can identify different constructors. This constructor is called by assigning the corresponding default value to each parameter. Alternatively, you can use theSuper.Instantiation.UNSAFE` strategy for creating classes that utilize Java's inner classes to create auxiliary types without calling the constructor. However, this method is not always portable to non-Oracle JVMs and may not be available in future JVM releases. As of today, the inner classes used in this dangerous instantiation method are found in almost every JVM implementation.

In addition, you may have already noticed that the LoggerInterceptor above declares a checked exception. On the other hand, the instrumented source method that calls this method does not declare a Checked ʻException`](http://docs.oracle.com/javase/tutorial/essential/exceptions/declaring.html). The Java compiler usually refuses to compile such calls. However, in contrast to the compiler, the Java runtime does not treat checked exceptions differently than unchecked ones and allows this call. For this reason, we decided to ignore the checked exceptions and give them complete flexibility in their use. However, be aware that throwing undeclared checked exceptions from dynamically created methods can confuse users of your application.

There is one more thing to note about the method delegation model. Static typing is great for implementing methods, but strict types can limit code reuse. To understand why, consider the following example.

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

The methods in the above class describe two similar signatures with incompatible types, so it is usually not possible to instrument both methods using a single interceptor method. Instead, you need to provide two different target methods with different signatures just to satisfy the static type check. To overcome this limitation, Byte Buddy allows you to annotate methods and method parameters with @RuntimeType.

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

You can now provide a single intercept method for both source methods using the target method above. Byte Buddy also allows you to box and unbox primitive values. However, using @ RunType comes at the expense of giving up type safety, so a mix of incompatible types can result in a ClassCastException.

As an equivalent to @ SuperCall, Byte Buddy comes with the @ DefaultCall annotation, which allows you to call the default method instead of calling the method's super method. A method with this parameter annotation is considered a binding only if the intercepted method is declared as the default method by the interface directly implemented by the instrumented type. Similarly, the @SuperCall annotation prevents method binding if the instrumented method does not define a non-abstract supermethod. However, if you want to call the default method on a particular type, you can specify the targetType property of @ DefaultCall on a particular interface. In this specification, Byte Buddy inserts a proxy instance that calls the default method of the specified interface type if it exists. Otherwise, target methods with parameter annotations are not considered delegates. Obviously, the default method calls are only available for classes defined in Java 8 and later class file versions. Similarly, in addition to the @Super annotation, there is a @Default annotation that injects a proxy to explicitly call a particular default method.

We have already mentioned that custom annotations can be defined and registered in any MethodDelegation. Byte Buddy is ready to use, but comes with one annotation that still needs to be explicitly installed and registered. You can use the @Pipe annotation to forward an intercepted method call to another instance. The @Pipe annotation is not pre-registered with MethodDelegation because the Java Class Library does not come with the appropriate interface types prior to Java 8 that define the function type. Therefore, you must explicitly specify the type using a single non-static method that takes ʻObject as an argument and returns another ʻObject as a result. You can use generic types as long as the method type is bound by the ʻObjecttype. Of course, if you're using Java 8, theFunction` type is an executable option. When you call a method with a parameter argument, Byte Buddy casts the parameter to the method's declarative type and calls the intercept method with the same arguments as the original method call. Before looking at the example, let's define a custom type that can be used in Java 5 and above.

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

You can use this type to implement a new solution that records access to the MemoryDatabase above by forwarding method calls 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();

In the above example, the call will only be forwarded to another instance created locally. However, the advantage over subclassing a type and intercepting a method is that you can extend an existing instance this way. In addition, you typically register interceptors at the instance level instead of registering static interceptors at the class level.

So far, we've seen a lot of MethodDelegation implementations. But before we move on, let's take a closer look at how Byte Buddy chooses the target method. We've already seen how Byte Buddy solves the most specific method by comparing parameter types, but there are others. After Byte Buddy identifies a suitable candidate method for binding to a particular source method, it delegates the solution to the chain of ʻAmbiguityResolvers`. Again, you are free to implement your own ambiguity solution that can complement or replace the Byte Buddy defaults. Without such changes, the ambiguity resolution chain attempts to identify its own target method by applying the following rules in the same order as:

You can assign an explicit priority to a method by annotating it with @BindingPriority. If one method has a higher priority than another, the higher priority method always takes precedence over the lower priority method. In addition, methods annotated by @IgnoreForBinding are not considered target methods. If the source and target methods have the same name, this target method takes precedence over other target methods with different names. If two methods use @Argument to bind the same parameters of the source method, the method with the most specific parameter type is considered. In this regard, it does not matter whether the annotations are provided explicitly or implicitly by not annotating the parameters. The resolution algorithm works similar to the Java compiler's algorithm for resolving calls to overloaded methods. If the two types are equally identified, the method that binds more arguments is considered the target. If you need to assign arguments to a parameter without considering the parameter type at this resolution stage, you can do so by setting the bindingMechanic attribute of the annotation to BindingMechanic.ANONYMOUS. In addition, the non-target parameter must be unique for each index value of each target method for the resolution algorithm to work. If the target method has more parameters than the other target methods, the former takes precedence over the latter. So far, we've just delegated method calls to static methods by naming a specific class, such as MethodDelegation.to (Target.class). However, you can also delegate to an instance method or constructor.

You can delegate a method call to any instance method of the Target class by callingMethodDelegation.to (new Target ()). This includes methods defined anywhere in the instance's class hierarchy, including methods defined in the ʻObjectclass. You may want to limit the range of candidate methods that you can do by callingfilter (ElementMatcher) on MethodDelegation to apply a filter to method delegation. The ʻElementMatcher type is the same as previously used to select source methods within Byte Buddy's domain-specific language. Instances that are subject to method delegation are stored in static fields. Like the fixed value definition, this requires a TypeInitializer definition. Instead of storing the mandate in a static field, you can also define the use of any field with MethodDelegation.toField (String). The argument specifies the field name to which all method delegations are forwarded. Be sure to assign a value to this field before calling a method on an instance of such a dynamic class. Otherwise, the method delegation will be NullPointerException. You can use method delegation to build an instance of a particular type. Calling an intercepted method by using MethodDelegation.toConstructor (Class) returns a new instance of the specified target type. As you have just learned, MethodDelegation examines annotations to tune its binding logic. These annotations are specific to Byte Buddy, but this does not mean that the annotated class will depend on Byte Buddy in any way. Instead, the Java runtime simply ignores annotation types that are not found in the classpath when the class is loaded. This means that Byte Buddy is no longer needed after the dynamic class was created. That is, you can load a dynamic class and the type that delegates its method calls into another JVM process, even if you don't have a Byte Buddy in your classpath.

There are some predefined annotations that you can use with MethodDelegation, but I'll briefly explain them. If you want to know more about these annotations, you can find more information in the documentation in your code. These annotations are as follows:

Calling a super method

As the name implies, the SuperMethodCall implementation can be used to call a super implementation of a method. At first glance, a single call to the super-implementation isn't very useful because it just duplicates the existing logic, not modifies the implementation. However, you can change the method annotations and their parameters by overriding the method. This is discussed in the next section. However, another reason to call a super method in Java is to define a constructor that must always call another constructor of supertype or its own type.

So far, we have simply assumed that a dynamic type constructor always resembles its direct supertype constructor. As an example, we can call

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

Create a subclass of ʻObject with a single default constructor defined to simply call the default constructor of its direct super-constructor ʻObject, however, this behavior is not specified by Byte Buddy. Instead, the above code is a shortcut to call.

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

ConstructorStrategy creates a set of predefined constructors for any class. In addition to the above strategy of copying each visible constructor of a dynamic type direct superclass, there are three other predefined strategies. An exception is thrown if no such constructor exists, and if there is only one that mimics the supertype public constructor.

Within the Java class file format, constructors are generally no different from methods. Therefore, Byte Buddy can handle them as they are. However, the constructor must contain a hard-coded call to another constructor so that it can be accepted by the Java runtime. As a result, most predefined implementations other than SuperMethodCall cannot create a valid Java class when applied to a constructor.

However, you can define your own constructor by implementing a custom ConstructorStrategy by using a custom implementation, or by defining individual constructors in Byte Buddy's domain-specific language using the defineConstructor method. I will. In addition, we plan to add new functionality to Byte Buddy to define more complex constructors as-is.

For class rebasing and class redefinition, we'll keep the constructor as well as the ConstructorStrategy specification obsolete. Instead, in order to copy the implementations of these retained constructors (and methods), you must specify ClassFileLocator, which allows you to search the original class file that contains these constructor definitions. Byte Buddy does its best to identify the location of the original class files on its own. For example, by querying the corresponding ClassLoader or by looking up the application's classpath. Lookups, however, may not be successful when dealing with customary class loaders. You can then provide a custom ClassFileLocator.

Calling a default method

In the version 8 release, the Java programming language introduced default methods for interfaces. In Java, default method calls have a syntax similar to super method calls. The only difference is that the default method call specifies the interface that defines the method. This is necessary because the default method call can be ambiguous if the two interfaces define methods with the same signature. Therefore, Byte Buddy's DefaultMethodCall implementation receives a list of prioritized interfaces. When intercepting a method, DefaultMethodCall calls the default method on the interface mentioned first. As an example, suppose you want to implement the following two interfaces:

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

If you create a class that implements both interfaces and implement the qux method to call the default method, this call will both call the default method defined in the First or Second interface. Can be expressed. However, by overriding the First interface with DefaultMethodCall, Byte Buddy recognizes that it must call the methods of this latter interface instead of the alternate interface.

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

Java classes defined in versions of class files prior to Java 8 do not support default methods. In addition, it should be noted that Byte Buddy imposes weaker requirements on the callability of default methods compared to the Java programming language. Byte Buddy only needs an interface for the default methods implemented by the most specific classes in the type hierarchy. Other than the Java programming language, this interface does not have to be the most specific interface implemented by the superclass. Finally, if you don't expect an ambiguous default method definition, you can always use DefaultMethodCall.unambiguousOnly () to receive an implementation that throws an exception at the discovery of an ambiguous default method call. This same behavior is that the default method call is ambiguous between non-prioritized interfaces, and prioritization DefaultMethodCall if no prioritized interface is found that defines a method with a compatible signature. It is also displayed with .

Calling a specific method

In some cases, the above implementation is not enough to implement more custom behavior. For example, you may want to implement a custom class that has explicit behavior. For example, you can implement the following Java class with a constructor that does not have a super constructor with the same arguments.

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

The previous implementation of SuperMethodCall could not implement this class because the ʻObject class does not define a constructor with ʻint as a parameter. Alternatively, you can explicitly call the ʻObject` super constructor.

new ByteBuddy()
  .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
  .defineConstructor(Arrays.<Class<?>>asList(int.class), Visibility.PUBLIC)
  .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()))
  .make()

In the code above, I created a simple subclass of ʻObject that defines a single constructor that takes a single unused ʻint parameter. The latter constructor is implemented by an explicit method call to the ʻObject` super constructor.

The implementation of MethodCall can also be used when passing arguments. These arguments are explicitly passed as values, as instance field values that need to be set manually, or as specified parameter values. The implementation also allows the method to be called on an instance other than the one that is instrumented. In addition, you can build a new instance from the intercepted method. The documentation for the MethodCall class provides detailed information about these features.

Accessing fields

You can use FieldAccessor to implement methods that read and write field values. To be compatible with this implementation, the method must do one of the following:

Creating such an implementation is easy: just call FieldAccessor.ofBeanProperty (). However, if you don't want to derive the field name from the method name, you can explicitly specify the field name using FieldAccessor.ofField (String). With this method, the only argument defines the name of the field to be accessed. If desired, you can use it to define a new field even if such a field does not already exist. When accessing an existing field, you can specify the type in which the field is defined by calling the ʻin` method. In Java, it is legal to define fields in some classes in the hierarchy. In this process, the fields of a class are hidden by the field definitions of its subclasses. Without an explicit location for the class of such a field, Byte Buddy will start with the most specific class and follow the class hierarchy to access the first field encountered.

Let's look at an example application for FieldAccessor. In this example, suppose you receive ʻUserType that you want to subclass at runtime. For this purpose, register an interceptor for each instance represented by the interface. In this way we can provide different implementations according to our actual requirements. This latter implementation can be exchanged by calling a method on the ʻInterceptionAccessor interface on the corresponding instance. To create an instance of this dynamic type, I don't want to use further reflection, but I call a method of ʻInstanceCreator` that acts as an object factory. The following types are similar to this setting:

class UserType {
  public String doSomething() { return null; }
}
 
interface Interceptor {
  String doSomethingElse();
}
 
interface InterceptionAccessor {
  Interceptor getInterceptor();
  void setInterceptor(Interceptor interceptor);
}
 
interface InstanceCreator {
  Object makeInstance();
}

You've already learned how to use MethodDelegation to intercept methods in a class. You can use the latter implementation to define a delegation to an instance field and name this field interceptor. In addition, it implements the ʻInterceptionAccessorinterface and intercepts all methods of the interface to implement the accessor for this field. By defining theBean property accessor, you can implement the getterofgetInterceptor and the setterofsetInterceptor`.

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();

The new dynamicUserType allows you to implement the InstanceCreator interface to become a factory for this dynamic type. Again, I'm using the known Method Delegation to call the dynamic type default constructor.

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

Note that you need to use the dynamicUserType classloader to load the factory. Otherwise, this type will not appear in the factory on load.

You can use these two dynamic types to finally create a new instance of dynamically extended ʻUserTypeand define a custom interceptor for that instance. Let's finish this example by applyingHelloWorldInterceptor` to the instance we just created. Note that both the field accessor interface and the factory have made it possible to do this without using reflections.

class HelloWorldInterceptor implements Interceptor {
  @Override
  public String doSomethingElse() {
    return "Hello World!";
  }
}
 
UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());

Miscellaneous

In addition to the implementations described so far, Byte Buddy includes several other implementations.

Annotations

You learned how Byte Buddy relies on annotations to provide some of its functionality. And Byte Buddy isn't the only Java application with an annotation-based API. To integrate dynamically created types with such applications, Byte Buddy allows you to define annotations for the created types and their members. Before we dive into the details of how to assign annotations to dynamically created types, let's 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();

Annotations are internally represented as interface types, as suggested by the Java @ interface keyword. As a result, annotations can be implemented by Java classes like a normal interface. The only difference from the interface implementation is the implicit ʻannotationType` method of annotations that determines the annotation type that the class represents. The latter method usually returns an implemented annotation-type class literal. The other annotation properties are implemented as if they were interface methods. However, keep in mind that the implementation of the annotation method requires repeating the default value of the annotation.

Defining dynamically created class annotations is especially important if a class needs to act as a subclass proxy for another class. Subclass proxies are often used to implement cross-cutting concerns when subclasses need to mimic the original class as transparently as possible. However, as long as this behavior is explicitly required by defining the annotation in @ Inherited, the annotation of the class will not be retained in its subclasses. You can easily create a subclass proxy that holds the annotations of the base class by using Byte Buddy to call the attribute methods of Byte Buddy's domain-specific language. This method expects TypeAttributeAppender as an argument. The Type Attribute Appender provides a flexible way to define annotations for dynamically created classes based on their base class. For example, if you pass TypeAttributeAppender.ForSuperType, the class annotations will be copied to the dynamically created subclass. Annotations and type attribute appenders are additive and you cannot define annotation types more than once for any class.

Method and field annotations are defined in the same way as the type annotations just described. Method annotations can be defined as the final statement in Byte Buddy's domain-specific language for implementing the method. Similarly, fields can be annotated after definition. Let's look at the example again.

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 code example above overrides the toString method and annotates the overridden method with RuntimeDefinition. In addition, the created type defines a field foo with the same annotation, and also defines the latter annotation on the created type itself.

By default, the ByteBuddy configuration does not predefine annotations for dynamically created types or type members. However, this behavior can be changed by specifying the default TypeAttributeAppender, MethodAttributeAppender, or FieldAttributeAppender. Note that such a default appender is not additive and replaces the previous value.

When defining a class, it may be desirable not to load the annotation type or its property type. For this purpose, you can use ʻAnnotationDescription.Builder`, which provides a fluent interface for defining annotations without triggering class loading, but at the expense of type safety. However, all annotation properties are evaluated at run time.

By default, Byte Buddy includes all annotation properties in the class file, including the default properties implicitly specified by the default values. However, this behavior can be customized by providing a ʻAnteationFilter to the ByteBuddy` instance.

Type annotations

Byte Buddy publishes and writes type annotations introduced as part of Java 8. Type annotations can be accessed as annotations declared by the TypeDescription.Generic instance. If you need to add a type annotation to a generic field or method type, you can use TypeDescription.Generic.Builder to generate the annotation type.

Attribute appenders

Java class files can contain arbitrary custom information as so-called attributes. Such attributes can be included using Byte Buddy by using * AttributeAppender on the type, field, or method. However, attribute appenders can also be used to define methods based on the information provided by the intercepted type, field, or method. For example, when overriding a method in a subclass, you can copy all the annotations for the intercepted method.

class AnnotatedMethod {
  @SomeAnnotation
  void bar() { }
}

new ByteBuddy()
  .subclass(AnnotatedMethod.class)
  .method(named("bar"))
  .intercept(StubMethod.INSTANCE)
  .attribute(MethodAttributeAppender.ForInstrumentedMethod.INSTANCE)

The above code overrides the bar method of the ʻAnnotatedMethod` class, but copies all annotations (including parameter or type annotations) of the overridden method.

The same rules may not apply when a class is redefined or rebased. By default, ByteBuddy is set to retain annotations for rebased or redefined methods even if the method is intercepted as described above. However, this behavior can be modified so that Byte Buddy discards existing annotations by setting the ʻAnnotationRetention strategy to DISABLED`.

Custom method implementations

The previous section described the Byte Buddy standard API. None of the features described so far require knowledge or explicit representation of Java bytecode. However, if you need to create custom bytecode, go to the API of ASM, a low-level bytecode library with Byte Buddy built on it. It can be created by direct access. However, different versions of ASM are not compatible with other versions, so you will need to repackage Byte Buddy into your namespace when you release your code. Otherwise, the application can introduce incompatibilities for other uses of Byte Buddy when different dependencies anticipate different versions of Byte Buddy based on different versions of ASM. You can find more information on maintaining your dependency on Byte Buddy on the Front Page (https://bytebuddy.net/#dependency).

The ASM library comes with a good documentation on Java bytecode and library usage (http://download.forge.objectweb.org/asm/asm4-guide.pdf). Therefore, please refer to this document in case you want to know more about Java bytecode and the ASM API. Instead, I'll briefly introduce the JVM execution model and ASM's API adaptation with Byte Buddy.

Every Java class file is made up of multiple segments. The core segments can be roughly classified as follows.

Fortunately, the ASM library takes responsibility for establishing a proper constant pool when creating a class. This leaves the only non-trivial element in the implementation of the method represented by an array of execution instructions, each encoded as a single byte. These instructions are processed by the virtual stack machine when the method is called. As a simple example, consider a method that calculates and returns the sum of two primitive integers 10 and 50. The Java bytecode for this method looks like this:

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

The Java bytecode array mnemonics above (http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings) begin by pushing both numbers onto the stack using the LDC instruction. Note how this execution order differs from the order in which addition is expressed in Java source code written as infix notation 10 + 50. Top-level value currently found on the stack This addition is represented by ʻIADD and consumes two top-level stack values that are both expected to be primitive integers. In the process, it adds these two values and pushes the result to the top of the stack. Finally, the ʻIRETURN statement consumes this calculation and returns it from the method, leaving an empty stack.

We've already mentioned that all primitive values referenced in a method are stored in the class's constant pool. This also applies to the numbers 50 and 10 referenced in the above method. Every value in the constant pool is assigned a 2-byte long index. Suppose the numbers 10 and 50 are stored in indexes 1 and 2. The above method is expressed as follows, along with the byte value of the above mnemonic, which is 0x12 for LDC, 0x60 for ʻIADD, and 0xAC for ʻIRETURN. Raw byte instruction:

12 00 01
12 00 02
60
AC

For compiled classes, this exact byte sequence is in the class file. However, this description is not yet sufficient to fully define the method implementation. To reduce the execution time of Java applications, each method needs to inform the Java virtual machine of the size required for the execution stack. For the above method, which comes without a branch, we've already seen that the stack has up to two values, so this is fairly easy to determine. However, in more complex ways, providing this information can easily be a complex task. To make matters worse, the stack values can be different sizes. Both the long and double values consume two slots, while the other values consume one. As if this is not enough, the Java virtual machine also needs information about the size of all local variables in the method body. All such variables in a method are stored in an array that also contains arbitrary method parameters and this references to non-static methods. Again, the long and double values consume two slots.

Obviously, tracking all this information makes Byte Buddy a simplified abstraction because manual assembly of Java bytecode is tedious and error-prone. Within Byte Buddy, stack instructions are included in the implementation of the StackManipulation interface. All implementations of stack operations combine an instruction to change a given stack with information about the effect of this instruction on the size. You can easily combine any number of such instructions into a common instruction. To demonstrate this, let's first implement the StackManipulation of 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 ʻapply method above, we can see that this stack operation executes the ʻIADD instruction by calling the associated method in the ASM method visitor. In addition, this method represents that the instruction reduces the current stack size by one slot. The second argument of the created Size instance is 0. This means that this instruction does not require a specific minimum stack size to calculate the intermediate result. In addition, any Stack Manipulation can be described as invalid. This behavior can be used for more complex stack operations, such as object assignments that can break type constraints. Later in this section, we'll see examples of invalid stack operations. Finally, note that the stack operation is described as a Singleton Enum (http://en.wikipedia.org/wiki/Singleton_pattern#The_Enum_way). Using these immutable and functional stack operation descriptions has proven to be a good practice for internal implementations of Byte Buddy. We recommend that you follow the same approach.

You can implement a method by combining the above IntegerSum with the predefined IntegerConstant and MethodReturn stack operations. Within Byte Buddy, the method implementation is included in ByteCodeAppender. It is implemented 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 method of interest, first verify that the instrumented method actually returns a primitive integer. Otherwise, the created class will be rejected by the JVM validator. It then loads the two numbers 10 and 50 onto the execution stack, applies the sum of these values, and returns the result of the calculation. Wrapping all these instructions in a composite stack operation ensures that you get the aggregate stack size needed to perform this set of stack operations. Finally, it returns the overall size requirement for this method. The first argument of the returned ByteCodeAppender.Size reflects the size required for the execution stack mentioned above to be included in StackManipulation.Size. In addition, the second argument reflects the size required for the local variable array. This is similar to this reference because we haven't simply defined the required size for the method parameters and local variables here.

The implementation of this aggregation method is ready to provide a custom implementation of this method that can be provided 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;
  }
}

Each implementation is queried in two steps. First, the implementation has the opportunity to modify the created class by adding additional fields or methods to the prepare method. In addition, this preparation allows the implementation to register the TypeInitializer learned in the previous section. If no such preparation is required, it is sufficient to return the unchanged ʻInstrumentedType provided as an argument. Implementations should generally not return individual instances of an instrumented type, but should call an instrumented type appender method that has all prefixes. After the implementation is prepared to create a particular class, the ʻappender method is called to get the ByteCodeAppender. This appender is then also queried for the method selected for interception by the specified implementation, and the method registered during the implementation's call to the prepare method.

Note that Byte Buddy calls each implementation's prepare and ʻappender methods only once during the class creation process. This is guaranteed no matter how many times the implementation is registered for use in creating the class. In this way, the implementation can avoid verifying whether the field or method is already defined. In the process, Byte Buddy compares the ʻImplementations instances with the hashCode and ʻequals` methods. In general, all classes used by Byte Buddy should provide a meaningful implementation of these methods. The fact that enumerations are attached to such implementations by definition is another good reason for them to be used.

Now let's see how SumImplementation works.

abstract class SumExample {
  public abstract int calculate();
}
 
new ByteBuddy()
  .subclass(SumExample.class)
    .method(named("calculate"))
    .intercept(SumImplementation.INSTANCE)
  .make()

Congratulations. We have extended Byte Buddy to implement a custom method that calculates and returns the sum of 10 and 50. Of course, this example implementation is not very practical. However, you can easily implement more complex implementations on top of this infrastructure. After all, if you feel you've created something useful, consider Contribute to your implementation (https://bytebuddy.net/develop). I look forward to hearing from you.

Before proceeding to customize the other components of Byte Buddy, we need to briefly explain the use of jump instructions and the so-called Java stack frame issue. Starting with Java 6, jump instructions used to implement, for example, ʻiforwhile` statements require some additional information to speed up the JVM validation process. This additional information is called a stack map frame. The stack map frame contains information about all the values found in the execution stack of any target of the jump instruction. Providing this information can save the JVM verifier some work, but for now it's up to us. For more complex jump instructions, providing the correct stack map frame is a fairly difficult task, and many code generation frameworks always have considerable problems creating the correct stack map frame. So how do you deal with this problem? In fact, we simply aren't. Byte Buddy's philosophy is that code generation should only be used as an adhesive between the unknown type hierarchy at compile time and the custom code that needs to be injected into these types. Therefore, the actual code generated should remain as restricted as possible. Whenever possible, conditional statements should be implemented and compiled in the JVM language of your choice, and then bound to a particular method with minimal implementation. A good side effect of this approach is that Byte Buddy users can work with regular Java code or use familiar tools such as debuggers and IDE code navigators. This is not possible with generated code that does not have a source code representation. However, if you need to use jump instructions to create bytecode, be sure to use ASM to add the correct stack map frame, as Byte Buddy will not automatically include them.

Creating a custom assigner

In the previous section, we explained that the built-in implementation of Byte Buddy relies on ʻAssigner to assign values to variables. In this process, ʻAssigner can apply the conversion of one value to another by issuing the appropriate StackManipulation. In doing so, Byte Buddy's built-in assistant provides, for example, automatic boxing of primitive values and their wrapper types. In the most common case, the value can be assigned directly to the variable. However, in some cases, what can be expressed by returning an invalid StackManipulation from the assigner cannot be assigned at all. A regular implementation of invalid assignments is provided by Byte Buddy's ʻIllegalStackManipulation` class.

To illustrate how to use a custom assigner, implement an assigner that assigns values only to string 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);
    } else {
      return StackManipulation.Illegal.INSTANCE;
    }
  }
}

The above implementation first verifies that the input value is not a primitive type and that the target variable type is a String type. If these conditions are not met, ʻAssigner issues ʻIllegalStackManipulation to invalidate the attempted assignment. Otherwise, it identifies the object type toString method by its name. Then use Byte Buddy's MethodInvocation to create a StackManipulation that virtually calls this method by source type. Finally, you can integrate this custom ʻAssigner with, for example, the 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, the string value 42 is returned. This is only possible by calling the toString method and using a custom assigner that converts the ʻInteger type to String. Further wrapping the custom assigner with the built-in PrimitiveTypeAwareAssigner that performs automatic boxing of the provided primitive ʻint to a wrapper type before delegating this wrapped primitive value assignment to its inner assigner. Please note. Other built-in assigners are VoidAwareAssigner and ReferenceTypeAwareAssigner. Be sure to implement the meaningful hashCode and ʻequals methods in your custom acais. These methods are typically called from the corresponding methods in ʻImplementation that are using a particular assigner. Also, implement the assigner as a singleton enumeration to avoid doing this manually.

Creating a custom parameter binder

We have already mentioned in the previous section that it is possible to extend the MethodDelegation implementation to handle user-defined annotations. For this purpose, you need to provide a custom ParameterBinder that knows how to handle the given annotation. As an example, I would like to define an annotation simply for the purpose of inserting a fixed string into an annotated parameter. First, define such a StringValue annotation.

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

You need to set the appropriate RuntimePolicy to make the annotation visible at run time. Otherwise, the annotation will not be retained at run time and Byte Buddy will not have a chance to find it. By doing so, the value property above contains the string that will be assigned as the value to the annotated parameter.

You need to create a corresponding ParameterBinder that can use custom annotations to create a StackManipulation that represents the binding for this parameter. This parameter binder is called every time and the corresponding annotation is found on the parameter by MethodDelegation. It's easy to implement a custom parameter binder for the annotations in this example.

enum StringValueBinder
    implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> {
 
  INSTANCE; // singleton
 
  @Override
  public Class<StringValue> getHandledType() {
    return StringValue.class;
  }
 
  @Override
  public MethodDelegationBinder.ParameterBinding<?> bind(AnnotationDescription.Loaded<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.loadSilent().value());
    return new MethodDelegationBinder.ParameterBinding.Anonymous(constant);
  }
}

First, the parameter binder verifies that the target parameter is actually of type String. Otherwise, it throws an exception notifying the user of the annotation of the illegal placement of this annotation. Otherwise, simply create a TextConstant that represents loading the constant stack string onto the run stack. This StackManipulation is finally wrapped as an anonymous ParameterBinding returned by the method. Alternatively, you may have specified the ʻUnique or ʻIllegal parameter binding. Unique bindings are identified by any object that allows you to get this binding from ʻAmbiguityResolver. In a later step, such a resolver can check if the parameter binding is registered with some unique identifier and then determine if this binding is better than other successfully bound methods. I can do it. With illegal binding, you can tell Byte Buddy that certain pairs of source and target` methods are incompatible and cannot be bound together.

This is already all the information you need to use custom annotations in your MethodDelegation implementation. After receiving the ParameterBinding, make sure that the value is bound to the correct parameter, or discard the current pair of source and target methods as unbound. In addition, it will allow ʻAmbiguityResolvers` to examine unique bindings. Finally, let's run this custom annotation.

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()

Specifying StringValueBinder as the only parameter binder replaces all default values. Alternatively, you can add a parameter binder to the one already registered. If the ToStringInterceptor has only one target method, the dynamic class's intercepted toString method is bound to the call to the latter method. When the target method is called, Byte Buddy assigns the annotation string value as the only parameter of the target method.

Recommended Posts

[Translation] Byte Buddy Tutorial
Apache Shiro tutorial translation summary
Truffle Tutorial Slides Personal translation memo ①
[Introduction to Docker] Official Tutorial (Japanese translation)