The Java language comes with a comparatively strict type system. Java requires all variables and objects to be of a specific type and any attempt to assign incompatible types always causes an error. These errors are usually emitted by the Java compiler or at the very least by the Java runtime when casting a type illegally. Such strict typing is often desirable, for example when writing business applications. Business domains can usually be described in such an explicit manner where any domain item represents its own type. This way, we can use Java to build very readable and robust applications where mistakes are caught close to their source. Among other things, it is Java's type system that is responsible for Java's popularity in enterprise programming.
However, by enforcing its strict type system, Java imposes limitations that restrict the language's scope in other domains. For example, when writing a general-purpose library that is to be used by other Java applications, we are normally not able to reference any type that is defined in the user's application because these types are unknown to us when our library is compiled. In order to call methods or to access fields of the user's unknown code, the Java Class Library comes with a reflection API. Using the reflection API, we are able to introspect unknown types and to call methods or access fields. Unfortunately, the use of the reflection API has two significant downsides:
This is where runtime code generation can help us out. It allows us to emulate some features that are normally only accessible when programming in a dynamic languages without discarding Java's static type checks. This way, we can get the best of both worlds and additionally improve runtime performance. To get a better understanding of this problem, let us look at the example of implementing the mentioned method-level security library.
Business applications can grow large and sometimes it is difficult to keep an overview of call stacks in our application. This can become problematic when we have crucial methods in our application that should only be called under specific conditions. Imagine a business application that implements a reset functionality that allows deleting everything from the application's database.
class Service { void deleteEverything() { // delete everything ... } }
Such a reset should of course only be performed by administrators and never by a normal user of our application. By analyzing our source code, we could of course make sure that this will never happen. However, we can expect our application to grow and to be changed in the future. Therefore, we want to implement a tighter security model where the method invocation is guarded by an explicit check for the application's current user. We typically use a security framework to ensure that only an administrator can call the method.
For this purpose, assume that we are using a security framework with a public API as the following:
@Retention(RetentionPolicy.RUNTIME) @interface Secured { String user(); } class UserHolder { static String user; } interface Framework { <T> T secure(Class<T> type); }
In this framework, the Secured
annotation should be used to mark methods that can only be
accessed by a given user. The UserHolder
is used for globally defining which user is
currently logged into the application. The Framework
interface allows for the creation of
secured instances by calling the default constructor of a given type. Of course, this framework is overly
simple, but in principle this is how security frameworks like for example the popular
Spring Security work. A feature of this
security framework is that we preserve the user's types. By the contract of our framework interface, we
promise the user to return an instance of any type T
it receives. Thanks to this,
a user is able to interact with his own types as if the security framework did not exist. In a test
environment, a user could even create unsecured instances of his types and use these instances instead of the
secured ones. You will agree that this is really handy! Such frameworks are known to interact with
POJOs, plain old Java objects, a term that was coined for describing non-intrusive frameworks that do
not impose their own types upon their users.
Imagine for now that we knew that the type handed to the Framework
could only be T =
Service
and that the deleteEverything
method was annotated with
@Secured("ADMIN")
. This way, we could easily implement a secured version of this particular type by
simply subclassing it:
class SecuredService extends Service { @Override void deleteEverything() { if(UserHolder.user.equals("ADMIN")) { super.deleteEverything(); } else { throw new IllegalStateException("Not authorized"); } } }
With this additional class we could implement the framework as follows:
class HardcodedFrameworkImpl implements Framework { @Override public <T> T secure(Class<T> type) { if(type == Service.class) { return (T) new SecuredService(); } else { throw new IllegalArgumentException("Unknown: " + type); } } }
Of course, this implementation is not of much use. By the secure
method's signature we suggested
that the method can provide security for any type but in reality, we will throw an exception once we encounter
something else then the known Service
. Also, this would require our security library to know
about this particular Service
type when the library is compiled. Obviously, this is not a
feasible solution for implementing the framework. So how can we solve this problem? Well, since this is a
tutorial on a code generation library you will have guessed the answer: We create a subclass on demand
and at runtime when the Service
class first becomes known to our security framework by the
invocation of the secure
method. With code generation, we can take any given type, subclass it at
runtime and override the methods we want to secure. In our case, we override all methods that are annotated
with @Secured
and read the required user from the annotation's user
property. Many
popular Java frameworks are implemented using a similar approach.
Before we learn all about code generation and Byte Buddy, note that you should use code generation with care. Java types are something rather special to the JVM and are often not garbage collected. Therefore, you should never overuse code generation but only solve problems using generated code when it is the only way out. However, if you need to enhance unknown types as in the previous example, code generation is most likely your only option. Frameworks for security, transaction management, object-relational mapping or mocking are typical users of code generation libraries.
Of course, Byte Buddy is not the first library for code generation on the JVM. However, we believe that Byte Buddy knows some tricks the other frameworks cannot apply. The overall objective of Byte Buddy is to work declaratively, both by focusing on its domain specific language and the use of annotations. No other code generation library for the JVM we know of works this way. Nevertheless, you might want to have a look at some other frameworks for code generation to find out which one suites you best. Among others, the following libraries are prevalent in the Java space:
Evaluate the frameworks for yourself but we believe that Byte Buddy offers functionality and convenience that you
will otherwise search in vain. Byte Buddy comes with an expressive domain specific language that allows for the
creation of very custom runtime classes by writing plain Java code and by using strong typing for your own code.
At the same time, Byte Buddy is very open for customization and does not restrain you to the features that come out
of the box. If required, you can even define custom byte code for any implemented method. But even without knowing
what byte code is or how it works, you are able to do quite a lot without digging deep into the framework. Did
you for example have a look at the Hello World!
example? Using Byte
Buddy is that easy.
Of course, a pleasant API is not the only feature to consider when choosing a code generation library. For many applications, the runtime characteristics of generated code is more likely to determine a best choice. And beyond the runtime of the generated code itself, the runtime for creating a dynamic class can also be a concern. Claiming that We are the fastest! is as easy as it is hard to provide a valid metric for a library's speed. Still, we want to provide some metrics as a basic orientation. However, keep in mind that these results do not necessarily translate to your more specific use case where you should rather implement your own metrics.
Before discussing our metrics, let us look at the raw data. The following table displays an operation's average runtime in nanoseconds where the standard deviation is attached in braces:
Metric | baseline | Byte Buddy | cglib | Javassist | Java proxy | |||||
trivial class creation (1) | 0.003 | (0.001) | 142.772 | (1.390) | 515.174 | (26.753) | 193.733 | (4.430) | 70.712 | (0.645) |
interface implementation (2a) | 0.004 | (0.001) | 1'126.364 | (10.328) | 960.527 | (11.788) | 1'070.766 | (59.865) | 1'060.766 | (12.231) |
stub method invocation (2b) | 0.002 | (0.001) | 0.002 | (0.001) | 0.003 | (0.001) | 0.011 | (0.001) | 0.008 | (0.001) |
class extension (3a) | 0.004 | (0.001) | 885.983 5'408.329 |
(7.901) (52.437) |
1'632.730 | (52.737) | 683.478 | (6.735) | – | |
super method invocation (3b) | 0.004 | (0.001) | 0.004 0.004 |
(0.001) (0.001) |
0.021 | (0.001) | 0.025 | (0.001) | – |
Similarly to static compilers, code generation libraries face a trade-off between generating fast code and generating code fast. When choosing between these conflicting goals, Byte Buddy's primary focus lies on generating code with minimal runtime. Typically, type creation or manipulation is not a common step within any program and does not significantly impact any long-running application; especially since class loading or class instrumentation is the most time-consuming and unavoidable step when running such code.
The first benchmark in the above table measures a library's runtime for subclassing Object
without
implementing or overriding any methods. This gives us an impression of a library's general overhead in code generation.
In this benchmark, Java proxies perform better than other libraries due to optimizations that are only possible when
assuming to always extend an interface. Byte Buddy also checks classes for generic types and annotations, which causes
additional runtimes. This performance overhead is also visible in the other benchmarks for creating a class. Benchmark
(2a) shows the measured runtime for creating (and loading) a class that implements a single interface with 18 methods,
(2b) shows the execution time for the methods generated for this class. Similarly, (3a) shows a benchmark for extending a
class with the same 18 methods which are implemented.
Byte Buddy provides two benchmarks, due to an optimization that is possible for an interceptor that always executes the super method. Sacrificing some time during class creation, the execution time of a Byte Buddy-created classes typically reaches the baseline, meaning that the instrumentation creates no overhead at all. It should be noted that Byte Buddy outperforms any other code generation library also during class creation, if the metadata processing was disabled. As the runtime of code generation is however so minimal compared to a program's total runtime, such an opt-out is not available as it would gain very little performance to the sacrifice of complicating the library code.
Finally, note that our metrics measure the performance of Java code that was priorly optimized by a JVM's just in time compiler. If your code is only executed occasionally, the performance will be worse than it is suggested by the above metrics. In this case, your code is however not performance-critical to begin with. The code for these metrics is distributed together with Byte Buddy and you can run these metrics on your own computer where the above numbers might be scaled depending on your machine's processing power. For this reason, do not interpret the above numbers absolutely, but consider them as a relative measure comparing different libraries. When further developing Byte Buddy, we want to monitor these metrics in order to avoid performance penalties when adding new features.
In the following tutorial we will gradually explain the features of Byte Buddy. We will start with its more general features which are most likely used by a majority of users. We will then consider increasingly advanced topics and give a short introduction to Java byte code and the class file format. And don't be discouraged in case you fast forward to this later material! You can do almost anything by using Byte Buddy's standard API and without understanding any JVM specifics. For learning about the standard API, just read on.
Any type that is created by Byte Buddy is emitted by an instance of the ByteBuddy
class. Simply
create a new instance by calling new ByteBuddy()
and you are ready to go. Hopefully, you are using
an development environment where you get suggestions on the methods that you can call on a given object. This
way, you can avoid to manually look up a class's API in Byte Buddy's javadoc but have your IDE guide you through
the process. As mentioned before, Byte Buddy offers a domain specific language which intends to be as
human-readable as possible. Your IDE's hints will therefore point you into the right direction most of the time.
But enough of the talking, let us create a first class at a Java program's runtime:
DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .subclass(Object.class) .make();
As it is hopefully obvious, the above code example creates a new class that extends the Object
type.
This dynamically created type would be equivalent to a Java class that only extends Object
without
explicitly implementing any methods, fields or constructors. You might have noted that we did not even name the
dynamically generated type, something that normally is required when defining a Java class. Of course, you could
have easily named your type explicitly:
DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .subclass(Object.class) .name("example.Type") .make();
But what happens without the explicit naming? Byte Buddy lives and breaths of
convention over configuration and
provides you with defaults that we found convenient. As for the name of a type, the default Byte Buddy
configuration provides a NamingStrategy
which randomly creates a class name based on a dynamic
type's superclass name. Furthermore, the name is defined to be in the same package as the super class such
that package-private methods of the direct superclass are always visible to the dynamic type. If you for example
subclassed a type named example.Foo
, the generated name will be something like
example.Foo$$ByteBuddy$$1376491271
where the numeric sequence is random. An exception of this rule
is made when subclassing types from the java.lang
package where types such as Object
live. Java's security model does not allow custom types to live in this namespace. Therefore, such type names
are prefixed with net.bytebuddy.renamed
by the default naming strategy.
This default behavior might not be convenient for you. And thanks to the convention over configuration
principle, you can always alter the default behavior by your needs. This is where the ByteBuddy
class comes into place. By creating a new ByteBuddy()
instance, you create a default configuration.
By calling methods on this configuration, you can customize it by your individual needs. Let's try this:
DynamicType.Unloaded<?> dynamicType = new ByteBuddy() .with(new NamingStrategy.AbstractBase() { @Override protected String name(TypeDescription superClass) { return "i.love.ByteBuddy." + superClass.getSimpleName(); } }) .subclass(Object.class) .make();
In the above code example, we created a new configuration that differs from the default configuration in its
type naming strategy. The anonymous class is implemented to simply concatenate the string
i.love.ByteBuddy
and the base class's simple name. When subclassing the
Object
type, the dynamic type is therefore named i.love.ByteBuddy.Object
. Be however
careful when creating your own naming strategies! The Java virtual machine uses names to distinguish between types
which is why you want to avoid naming collisions. If you need to customize the naming behavior, consider
using Byte Buddy's built-in NamingStrategy.SuffixingRandom
which you can customize to include
a prefix that is more meaningful to your application than our default.
After seeing Byte Buddy's domain specific language in action, we need to have a short look at the way this language is implemented. The one detail you need to know about the implementation is that the language is built around immutable objects. As a matter of fact, almost every class that lives in the Byte Buddy namespace was made immutable and in the few cases we could not make a type immutable, we explicitly mention it in this class's javadoc. If you implement custom features for Byte Buddy, we recommend you to stick with this principle.
As an implication of the mentioned immutability, you must be careful when, for example, configuring
ByteBuddy
instances. You must avoid mistakes such as the following one:
ByteBuddy byteBuddy = new ByteBuddy(); byteBuddy.with(new NamingStrategy.SuffixingRandom("suffix")); DynamicType.Unloaded<?> dynamicType = byteBuddy.subclass(Object.class).make();
You might expect the dynamic type to be generated using the custom naming strategy new
NamingStrategy.SuffixingRandom("suffix")
that was (allegedly) defined. Instead of mutating the
instance that is stored in the byteBuddy
variable, the invocation of the
with
method returns a customized ByteBuddy
instance which is however
lost. As a result, the dynamic type is created using the default configuration which was originally created.
So far, we only demonstrated how Byte Buddy can be used to create a subclass of an existing class. The same API can however be used for enhancing existing classes. Such enhancement is available in two different flavours:
class Foo { String bar() { return "bar"; } }to return
"qux"
from the bar
method, the information that this method originally
returned "bar"
would be lost entirely.
Foo
could be rebased to something likeclass Foo { String bar() { return "foo" + bar$original(); } private String bar$original() { return "bar"; } }where the information that the
bar
method originally returned "bar"
is preserved
within another method and therefore remains accessible. When rebasing a class, Byte Buddy treats all method
definitions such as if you defined a subclass, i.e. it will call the rebased method if you attempt to
call a rebased method's super method implementation. But instead, it eventually flattens this hypothetical
super class into the rebased type displayed above.
Any rebasing, redefinition or subclassing is performed using an identical API which is defined by the
DynamicType.Builder
interface. This way, it is possible, for example, to define a class as a subclass
and to later alter the definition to represent a rebased class instead. This is achieved by merely changing a
single word of Byte Buddy's domain specific language. Applying either of the possible approaches
new ByteBuddy().subclass(Foo.class) new ByteBuddy().redefine(Foo.class) new ByteBuddy().rebase(Foo.class)
is handled transparently during further stages of the definition process, which is explained throughout the remainder of this tutorial. Because a subclass definition is a familiar concept to Java developers, all of the following explanations and examples of Byte Buddy's domain specific language are demonstrated by creating subclasses. However, keep in mind that all classes could similarly be defined by redefinition or by rebasing.
So far we only have defined and created a dynamic type but we did not make any use of it. A type that is
created by Byte Buddy is represented by an instance of DynamicType.Unloaded
. As the name
suggests, these types are not loaded into the Java virtual machine. Instead, classes created by Byte Buddy are
represented in their binary form, in the
Java class file format. This way,
it is up to you to decide what you want to do with a generated type. For example, you might want to run
Byte Buddy from a build script that only generates classes to enhance a Java application before it is deployed.
For this purpose, the DynamicType.Unloaded
class allows to extract a byte array that represents
the dynamic type. For convenience, the type additionally offers a saveIn(File)
method that allows
you to store a class in a given folder. Furthermore, it allows you to inject(File)
classes into
an existing jar file.
While directly accessing a class's binary form is straight forward, loading a type is unfortunately more
complex. In Java, all classes are loaded using a ClassLoader
. One example for such a class loader
is the bootstrap class loader which is responsible for loading the classes that are shipped within the Java class
library. The system class loader, on the other hand, is responsible for loading classes on the Java application's
class path. Obviously, none of these preexisting class loaders is aware of any dynamic class we have created.
To overcome this, we have to find other possibilities for loading a runtime generated class. Byte Buddy offers
solutions by different approaches out of the box:
ClassLoader
which is explicitly told about the existence of a
particular dynamically created class. Because Java class loaders are organized in hierarchies, we define this
class loader as the child of a given class loader that already exists in the running Java application. This way,
all types of the running Java program are visible to the dynamic type that was loaded with new
ClassLoader
.
ClassLoader
before attempting to directly load a
type of a given name. This implies that a class loader normally never loads a type in case that its parent
class loader is aware of a type with identical name. For this purpose, Byte Buddy offers the creation of a
child-first class loader which attempts to load a type by itself before querying its parent. Other than that,
this approach is similar to the approach just mentioned above. Note that this approach does not override a type
of a parent class loader but rather shadows this other type.
ClassLoader
. Usually, a class
loader is asked to provide a given type by its name. Using reflection, we can turn this principle around and
call a protected method to inject a new class into the class loader without the class loader actually knowing
how to locate this dynamic class.
Unfortunately, the above approaches have both their downsides:
ClassLoader
, this class loader defines a new namespace. As an implication,
it is possible to load two classes with identical name as long as these classes are loaded by two different
class loaders. These two classes are then never be considered as equal by a Java virtual machine, even if
both classes represent an identical class implementation. This rule for equality holds however also for Java
packages. This means that a class example.Foo
is not able to access package-private
methods of another class example.Bar
if both classes were not loaded with the same class loader.
Also, if example.Bar
extended example.Foo
, any overridden package-private methods
would become inoperative, but still delegate to the original implementations.
example.Foo
and example.Bar
.
If we injected example.Foo
into an existent class loader, this class loader might attempt to
locate example.Bar
. This lookup would however fail since the latter class was created dynamically
and is unreachable for the class loader into which we just injected the example.Foo
class.
Therefore, the reflective approach cannot be used for classes with circular dependencies that become
effective during class loading. Fortunately, most JVM implementations resolve referenced classes lazily on
their first active use which is why class injection normally works without these restrictions. Also, in
practice, classes that are created by Byte Buddy normally do not suffer from such circularity.
You might consider the chance of encountering circular dependencies to be of minor relevance since you
are creating one dynamic type at a time. However, the dynamic creation of a type might trigger the creation of
so-called auxiliary types. These types are created by Byte Buddy automatically to provide access to the dynamic
type you are creating. We learn more about auxiliary types in the following section, do not worry about
them for now. However, because of this, we recommend you to load dynamically created classes by creating a
specific ClassLoader
instead of injecting them into an existing one, whenever possible.
After creating a DynamicType.Unloaded
, this type can be loaded using a
ClassLoadingStrategy
. If no such strategy is provided, Byte Buddy infers such a strategy based on
the provided class loader. Then it creates a new class loader only for the bootstrap class loader, where no type can
be injected using reflection, which is otherwise the default. Byte Buddy provides several class loading strategies
out of the box, where each one follows the concepts described above. These strategies are defined in
ClassLoadingStrategy.Default
where the WRAPPER
strategy creates a new, wrapping
ClassLoader
, where the CHILD_FIRST
strategy creates a similar class loader with
child-first semantics and where the INJECTION
strategy injects a dynamic type using reflection.
Both the WRAPPER
and the CHILD_FIRST
strategies are also available in so-called
manifest versions where a type's binary format is preserved even after a class was loaded. These alternative
versions make the binary representation of a class loader's classes accessible via the
ClassLoader::getResourceAsStream
method. However, note that this requires these class loaders to
maintain a reference to the full binary representation of a class, what consumes space on a JVM's heap. Therefore,
you should only use the manifest versions if you plan to actually access the binary format. Since the
INJECTION
strategy works via reflection and without a possibility to change the semantics of the
ClassLoader::getResourceAsStream
method, it is naturally not available in a manifest version.
Let's look at such class loading in action:
Class<?> type = new ByteBuddy() .subclass(Object.class) .make() .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded();
In the above example, we have created and loaded a class. We used the WRAPPER
strategy for loading
the class which is suitable for most cases, as we mentioned it before. Finally, the getLoaded
method
returns an instance of a Java Class
that represents the dynamic class which is now loaded.
Note that when loading classes, the predefined class loading strategies are executed by applying the
ProtectionDomain
of the current execution context. Alternatively, all default strategies offer
the specification of an explicit protection domain by calling the with
method.
Defining an explicit protection domain is important when using security managers
(disabled in JDK 24 and marked for removal) or when working with classes
that are defined in signed jars.
In a previous section, we learned how Byte Buddy can be used in order to redefine or to rebase an existing class.
During the execution of a Java program, it is however often impossible to guarantee that a specific class is not
already loaded. Thanks to the
Java virtual machine's HotSwap feature, existing classes can however be redefined even after they are loaded.
This feature is made accessible by Byte Buddy's ClassReloadingStrategy
. Let us demonstrate this
strategy by redefining a class Foo
:
class Foo { String m() { return "foo"; } } class Bar { String m() { return "bar"; } }
Using Byte Buddy, we can now easily redefine the class Foo
to become Bar
.
The code below renames the class Bar
to Foo
.
After the redefinition, when we reference the latter, the former is referenced instead. Using
HotSwap, this redefinition will even apply for preexisting instances:
// Requires byte-buddy-agent dependency ByteBuddyAgent.install(); Foo foo = new Foo(); new ByteBuddy() .redefine(Bar.class) .name(Foo.class.getName()) .make() .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); assertThat(foo.m(), is("bar"));
HotSwap is only accessible using a
so-called Java
agent.
Such an agent can be installed by either specifying it on the startup of the Java virtual machine by using the
-javaagent
parameter where the parameter's argument needs to be Byte Buddy's agent jar which can be
downloaded from Maven Central. However,
when a Java application is run from a JDK-installation of the Java virtual machine, Byte Buddy can load a Java
agent even after application startup by ByteBuddyAgent.install()
. Because class redefinition
is mostly used to implement tooling or testing, this can be a very convenient alternative.
Since Java 9, an agent installation is also possible at runtime without a JDK-installation.
One thing that might first appear counter-intuitive about the above example is the fact that Byte Buddy is
instructed to redefine the Bar
type where the Foo
type is eventually redefined. The
Java virtual machine identifies types by their name and a class loader. Therefore, by renaming Bar
to Foo
and applying this definition, we eventually redefine the type we renamed Bar
into. It is of course equally possible to redefine Foo
directly without renaming a different type.
Using Java's HotSwap feature, there is however one huge drawback. Current implementations of HotSwap require that
the redefined classes apply the same class schema both before and after a class redefinition. This means that it
is not allowed to add methods or fields when reloading classes. We already discussed that Byte Buddy defines
copies of the original methods for any rebased class such that class rebasing does not work for the
ClassReloadingStrategy
. Also, class redefinition does not work for classes with an explicit class
initializer method (a static block within a class) because this initializer needs to be copied into an extra method
as well. Unfortunately OpenJDK has withdrawn from extending HotSwap
functionality, so there is no way to work around this limitation using the HotSwap feature. In the mean time,
Byte Buddy's HotSwap support can be used for corner-cases where it seems useful. Otherwise, class rebasing and
redefinition can be a convenient feature when enhancing existing classes from for example a build script.
With this realization about the limits of Java's HotSwap feature, one might think that the only meaningful
application of the rebase
and redefinition
instructions would be during build time.
By applying build-time manipulation, one can assert that a processed class is not loaded before its initial
class loading, simply because this class loading is accomplished in a different instance of the JVM. Byte Buddy
is however equally capable of working with classes that were not yet loaded. For this, Byte Buddy abstracts over
Java's reflection API such that a Class
instance is for example internally represented by an
instance of a TypeDescription
. As a matter of fact, Byte Buddy only knows how to process a provided
Class
by an adapter that implements the TypeDescription
interface. The big
advantage over this abstraction is that information on classes do not need to be provided by a
ClassLoader
but can be provided by any other sources.
Byte Buddy provides a canonical manner for getting hold of a class's TypeDescription
using a
TypePool
. A default implementation of such a pool is of course also provided. This
TypePool.Default
implementation parses the binary format of a class and represents it as the
required TypeDescription
. Similarly to a ClassLoader
, it maintains a cache for
represented classes which is also customizable. Also, it normally retrieves the binary format of a class from
a ClassLoader
, however without instructing it to load this class.
The Java virtual machine only loads a class on its first usage. As a consequence, we can for example safely redefine a class such as
package foo; class Bar { }
right at program startup before running any other code:
class MyApplication { public static void main(String[] args) { TypePool typePool = TypePool.Default.ofSystemLoader(); Class> bar = new ByteBuddy() .redefine(typePool.describe("foo.Bar").resolve(), // do not use 'Bar.class' ClassFileLocator.ForClassLoader.ofSystemLoader()) .defineField("qux", String.class) // we learn more about defining fields later .make() .load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION) .getLoaded(); assertThat(bar.getDeclaredField("qux"), notNullValue()); } }
By explicitly loading the redefined class before its first use in the assertion statement, we forestall the JVM's
built-in class loading. This way, the redefined definition of foo.Bar
is loaded and used throughout
our application's runtime. Note however that we do not reference the class by a class literal when we use the
TypePool
to provide a description. If we did use a class literal for foo.Bar
, the JVM
would have loaded this class before we had a chance to redefine it, and our redefinition attempt would be without
effect. Also, note that when working with unloaded classes, we further need to specify a
ClassFileLocator
which allows to locate a class's .class
file. In the example above, we simply
create a class file locator which scans the running application's class path for such files.
When an application grows bigger and becomes more modular, applying such a transformation at a specific program
point is of course a cumbersome constraint to enforce. And there is indeed a better way to apply such class
redefinitions on demand. Using a
Java agent,
it is possible to directly intercept any class loading activity that is conducted within a Java application. A
Java agent is implemented as a simple jar file with an entry point that is specified in this jar's
manifest file, as it is described under the linked resource. Using Byte Buddy, the implementation of such an
agent is straight forward by using an AgentBuilder
. Assuming that we defined a simple
annotation named ToString
as below:
@Retention(RetentionPolicy.RUNTIME) public @interface ToString { }
It would be trivial to implement toString
methods
for all annotated classes simply by implementing the Agent's premain
method as follows:
class ToStringAgent { public static void premain(String arguments, Instrumentation instrumentation) { new AgentBuilder.Default() .type(isAnnotatedWith(ToString.class)) .transform(new AgentBuilder.Transformer() { @Override public DynamicType.Builder> transform( DynamicType.Builder> builder, TypeDescription td, ClassLoader cl, JavaModule jm, ProtectionDomain pd) { return builder.method(named("toString")) .intercept(FixedValue.value("transformed")); } }).installOn(instrumentation); } }
The code of the premain
method can be even simplified by writing a Lambda Expression
if you are using JDK 8+:
new AgentBuilder.Default() .type(isAnnotatedWith(ToString.class)) .transform((builder, td, cl, jm, pd) -> builder.method(named("toString")).intercept(FixedValue.value("transformed"))) .installOn(instrumentation);
As a result of applying the above AgentBuilder.Transformer
, all toString
methods
of the annotated classes would now return transformed
. We will learn all about Byte Buddy's
DynamicType.Builder
in the upcoming sections, do not worry about this class for now. The above
code results of course in a trivial and meaningless application. Using this concept right, renders however a
powerful tool for easily implementing aspect-oriented programing.
Annotate the Bar
class we created before (or any other one) with @ToString
so that we can try our agent.
Now you can add a regular main
method to the ToStringAgent
class,
that manually calls the premain
method, getting an Instrumentation
instance
from ByteBuddyAgent.install()
:
public static void main(String[] args) { premain("", ByteBuddyAgent.install()); System.out.println(new Bar()); }
This way, it tries to install the agent in a running JVM, without requiring you to pass the -javaagent
parameter to the JVM.
However, that approach may not always work (check the ByteBuddyAgent.install()
docs for details).
Note that it is also possible to instrument classes that were loaded by the bootstrap class loader when using an
agent. However, this requires some preparation. First of all, the bootstrap class loader is represented by the
null
value which makes it impossible to load a class in this class loader using reflection. This
is however sometimes necessary to load helper classes into the instrumented class's class loader to support the
class's implementation. In order to load classes into the bootstrap class loader, Byte Buddy can create jar files
and add these files to the bootstrap class loader's load path. To make this possible, it is however required to
save these classes to disk. A folder for these classes can be specified using the
ClassReloadingStrategy.enableBootstrapInjection
method, which also takes an instance of the Instrumentation
interface in order to append the classes. Note that all user classes that are used by the instrumented class are
also required to be put on the bootstrap search path which is possible using the Instrumentation
interface.
Android uses a different class file format using dex files which are not in the layout of the Java class
file format. Furthermore, with the ART runtime
which succeeds the Dalvik virtual machine, Android
applications are compiled into native machine code before being installed on an Android device. As a result,
Byte Buddy cannot longer redefine or rebase classes as long as an applications is not explicitly deployed
together with its Java sources as there is otherwise no intermediate code representation to interpret. Byte
Buddy is however still capable to define new classes using a DexClassLoader
together with a built-in
dex compiler. For this purpose, Byte Buddy offers the byte-buddy-android
module which contains
the AndroidClassLoadingStrategy
which allows the loading of dynamically created classes from within
an Android application. In order to function, it requires a folder for writing temporary files and compiled
class files. This folder must not be shared among different applications as this is forbidden by Android's
security manager.
Byte Buddy is processing generic types as they are defined by the Java programming language. Generic types are not considered by the Java runtime which only processes the erasures of generic types. However, generic types are still embedded into any Java class file and are exposed by the Java reflection API. Therefore, it sometimes makes sense to include generic information into a generated class because the generic type information can effect the behavior of other libraries and frameworks. Embedding generic type information is also important when a class is persisted and processed as a library by the Java compiler.
When creating a subclass, implementing an interface or declaring a field or method, Byte Buddy accepts
a Java Type
instead of an erased Class
for the above reasons. Generic types can
also be defined explicitly by using the TypeDescription.Generic.Builder
. One important difference
of Java generic types to type erasures is the contextual meaning of type variables. A type variable of a
certain name, defined by some type, does not necessarily denote the same type when another type declares
the same type variable with the same name. Therefore, Byte Buddy rebinds all generic types that denote type
variables in the context of the generated type or method when a Type
instance is handed to the
library.
Byte Buddy also inserts
bridge methods
transparently when a type is created. Bridge methods are resolved by a MethodGraph.Compiler
which is a property of any ByteBuddy
instance. The default method graph compiler behaves like
the Java compiler and processes any class file's generic type information. For other languages than Java,
a different method graph compiler might however be appropriate.
Most types we created in the previous section did not define any fields or methods. However, by subclassing
Object
, the created class inherits the methods that are defined by its super class. Let us verify this
Java trivia and call the toString
method on an instance of the dynamic type. We can get hold of an
instance by calling the created class's constructor reflectively.
String toString = new ByteBuddy() .subclass(Object.class) .name("example.Type") .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() // Java reflection API .toString();
The implementation of the Object#toString
method returns the concatenation of the instance's fully
qualified class name and the hex representation of the instance's hash code. And in fact, invoking the
toString
method on the created instance returns something like example.Type@340d1fa5
.
Of course, we are not done here. The main motivation of creating dynamic classes is the ability to define new
logic. To demonstrate how this is done, let us start with something simple. We want to override the
toString
method and return Hello World!
instead of the previous default value:
String toString = new ByteBuddy() .subclass(Object.class) .name("example.Type") .method(named("toString")).intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() .toString();
The line we added to our code contains two instructions in Byte Buddy's domain specific language. The first
instruction is method
which allows us to select any number of methods that we want to override.
This selection is applied by handing over a ElementMatcher
which serves as a predicate to decide
for each overridable method if it should be overridden or not. Byte Buddy comes with a lot of predefined method
matchers which are collected in the ElementMatchers
class. Normally, you would import this class
statically such that the resulting code reads more naturally. Such a static import was also assumed for the
above example where we used the named
method matcher which selects methods by their exact names.
Note that the predefined method matchers are composable. This way, we could have described the method selection
in further detail such as by:
named("toString").and(returns(String.class)).and(takesArguments(0))
This latter method matcher describes the toString
method by its full Java signature and therefore
only matches this particular method. However, in the given context we know that there is no other method
named toString
with a different signature such that our original method matcher is sufficient.
After selecting the toString
method, the second instruction intercept
determines
the implementation that should override all methods of the given selection. In order to know how to implement a
method, this instruction requires a single argument of type Implementation
. In the above example,
we are making use of the FixedValue
implementation which ships with Byte Buddy. As suggested by
this class's name, it implements a method that always return a given value. We will have a more
detailed look at the FixedValue
implementation a little later in this section. Right now, let us
rather look a little closer at the method selection.
So far, we only intercepted a single method. In real applications, things might however be more complicated and we might want to apply different rules for overriding different methods. Let us look at an example of such a scenario:
class Foo { public String bar() { return null; } public String foo() { return null; } public String foo(Object o) { return null; } } Foo dynamicFoo = new ByteBuddy() .subclass(Foo.class) .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!")) .method(named("foo")).intercept(FixedValue.value("Two!")) .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!")) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance();
In the above example, we defined three different rules for overriding methods. When investigating the code, you
will notice that the first rule concerns any method that is defined by Foo
, i.e. all three
methods in the example class. The second rule matches both methods that are named foo
, a subset of
the previous selection. And the last rule only matches the foo(Object)
method which is a
further reduction of the former selection. But given this selection overlap, how does Byte Buddy decide which rule
is applied to which method?
Byte Buddy organizes rules for overriding methods in a stack form. This means that whenever you register a new rule for overriding a method, ít is pushed on the top of this stack and is always applied first until a new rule is added which will then be of even higher priority. For the above example, this means that:
bar()
method is first matched against named("foo").and(takesArguments(1))
and then against named("foo")
where both matching attempts turn out negative. Finally, the
isDeclaredBy(Foo.class)
matcher gives green light to override the bar()
method to
return One!
.
foo()
method is first matched against
named("foo").and(takesArguments(1))
first where the missing argument results in an unsuccessful
matching. After this, the named("foo")
matcher determines a positive match such that the
foo()
method is overridden to return Two!
.
foo(Object)
is immediately matched by the named("foo").and(takesArguments(1))
matcher such that the overridden implementation returns Three!
.
Because of this organization, you should always register more specific method matchers last. Otherwise, any less
specific method matcher that is registered afterward might prevent rules that you defined before from being
applied. Note that the ByteBuddy
provides an ignoreAlso
method that also takes a matcher, so that methods that are successfully matched are never overridden. By default,
Byte Buddy does not override any synthetic methods.
In some scenarios, you might want to define a new method that does not override a method of a super type or an
interface. This is also possible using Byte Buddy. For this purpose, you can call define(Method)
or defineMethod(name, ...)
where
you are able to define a signature. After defining a method, you are asked to provide an
Implementation
just as with a method that was identified by a method matcher. Note that method
matchers that are registered after a method's definition might supersede this implementation by the stacking
principle that we discussed before.
With define(Field)
or defineField(name, ...)
, Byte Buddy allows to define fields for a given type. In Java, fields are never
overridden but can only be shadowed. For this reason,
no field matching or the like is available.
With this knowledge on how methods are selected, we are ready to learn about how we can implement these methods.
For this purpose, we now look at predefined Implementation
implementations that ship with Byte Buddy.
Defining custom implementation is discussed in its own section and is only intended for users that require
very custom method implementations.
We have already seen the FixedValue
implementation in action. As the name suggests, methods
that are implemented by FixedValue
simply return a provided object. A class is able to remember
such an object in two different manners:
TypeInitializer
which can be configured to
execute such explicit initialization. When you instruct a DynamicType.Unloaded
to be loaded,
Byte Buddy automatically triggers its type initializer such that the class is ready for use. Therefore,
you do not normally need to worry about type initializers. However, if you want dynamic classes to
be loaded outside of Byte Buddy, it is important that you run their type initializers manually after these
classes are loaded. Otherwise, a FixedValue
implementation would for example return
null
instead of the required value because the static field was never assigned this value.
Many dynamic types might however not require explicit initialization. A class's type initializer can
therefore be queried for its liveliness by calling its isAlive
method. If you need to trigger
a TypeInitializer
manually, you find it exposed by the DynamicType
interface.
When you implement a method by FixedValue#value(Object)
, Byte Buddy analyzes the parameter's
type and define it to be stored in (i) the class pool of the dynamic type, if possible, or (ii) in a static field otherwise.
Note however that the instance that is returned by the selected methods might be of a
different object identity if the value was stored in the class pool. Therefore, you can instruct Byte Buddy to
always store an object in a static field by using FixedValue#reference(Object)
. The latter method
is overloaded such that you can provide the field's name as a second argument. Otherwise, a field name is derived
automatically from the object's hash code. An exception from this behavior is the null
value.
The null
value is never stored in a field but is simply represented by its literal expression.
You might wonder about type safety in this context. Obviously, you could define a method to return an invalid value:
new ByteBuddy() .subclass(Foo.class) .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0)) .make();
It would be difficult to prevent this invalid implementation by the compiler within Java's type system. Instead,
Byte Buddy will throw an IllegalArgumentException
when the type is created and the illegal assignment
of an integer to a method that returns a String
becomes effective. Byte Buddy tries its best to assure
that all its created types are legal Java types and fail fast by throwing an exception during the creation
of an illegal type.
Byte Buddy's assignment behavior is customizable. Again, Byte Buddy only provides a sane default which mimics
the assignment behavior of the Java compiler. Consequently, Byte Buddy allows an assignment of a type to any
of its super types and it will also consider to box primitive values or to unbox their wrapper representations.
Note however, that Byte Buddy does currently not fully support generic types and will only consider type erasures.
Therefore, it is possible that Byte Buddy causes heap
pollution. Instead of using the predefined assigner, you can always implement your own Assigner
which is capable of type transformations that are not implicit in the Java programming language. We will look
into such custom implementations in the last section of this tutorial. For now, we settle for mentioning that
you can define such custom assigners by calling withAssigner
on any FixedValue
implementation.
In many scenarios, returning a fixed value from a method is of course insufficient. For more flexibility, Byte
Buddy provides the MethodDelegation
implementation which offers maximal freedom in reacting to method
calls. A method delegation makes a method of the dynamically created type to forward any call to another
method which may live outside the dynamic type. When providing a reference to the target class where a method call will be delegated to,
the target methods must be static for the delegation to work (instance methods are also supported, as detailed further).
This way, a dynamic class's logic can be represented using
plain Java, while only the binding to another method is achieved by code generation. Before discussing the details,
let us look at an example of using a MethodDelegation
:
class Source { public String hello(String name) { return null; } } class Target { public static String hello(String name) { return "Hello " + name + "!"; } } String helloWorld = new ByteBuddy() .subclass(Source.class) .method(named("hello")).intercept(MethodDelegation.to(Target.class)) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() .hello("World");
In the example, we are delegating invocations of the Source#hello(String)
method to the
Target
type, such that the method returns Hello World!
instead of null
.
For this purpose, the MethodDelegation
implementation identifies an invokable method of the
Target
type that best matches the Source
method. In the above example, this is
trivial since the Target
type only defines a single static method where the method's parameters,
return type and name are conveniently identical to those of Source#name(String)
.
In reality, the decision for a delegation target method will most likely be more complex. So how does Byte Buddy
decide between methods if there is an actual choice? For this purpose, let us assume the Target
class
to be defined as follows:
class Target { public static String intercept(String name) { return "Hello " + name + "!"; } public static String intercept(int i) { return Integer.toString(i); } public static String intercept(Object o) { return o.toString(); } }
You might have noticed that the above methods are now all called intercept
. Byte Buddy does not require
target methods to be named equally to a source method. We will look closer into this matter shortly. More
importantly, if you ran the former example with the altered definition of Target
, you would
observe that the hello(String)
method was bound to intercept(String)
. But why
is that? Obviously, the intercept(int)
method cannot receive the String
argument of the
source method and is therefore not even considered as a possible match. However, this is not true for the
intercept(Object)
method which could be bound. In order to resolve this ambiguity, Byte Buddy once
again mimics the Java compiler by choosing the method binding with the most specific parameter types. Remember how
the Java compiler chooses a binding for an overloaded method! Since String
is more specific than
Object
, the intercept(String)
class is finally chosen among the three alternatives.
With the information so far, you might consider the method binding algorithm to be of a rather rigid nature.
However, we have not yet told the full story. So far we only observed another example of the convention over
configuration principle, which is open for change if the defaults do not fit the actual requirements. In reality,
the MethodDelegation
implementation works with annotations where a parameter's annotation decides
which value should be assigned to it. However, if no annotation is found, Byte Buddy treats a parameter as if
it was annotated with @Argument
. This latter annotation causes Byte Buddy to assign the n
-th
argument of the source method to the annotated target. When the annotation is not added explicitly, the value
of n
is set to the annotated parameter's index. According to this rule, Byte Buddy treats
void foo(Object o1, Object o2)
as if the all parameters were annotated as:
void foo(@Argument(0) Object o1, @Argument(1) Object o2)
As a result, the first and the second argument of the instrumented method are assigned to the interceptor. If the intercepted method does not declare at least two parameters or if the annotated parameter types are not assignable from the instrumented method's parameter types, the interceptor method in question is discarded.
Besides the @Argument
annotation, there are several other pre-defined annotations that can be used
with a MethodDelegation
:
@AllArguments
annotation (from net.bytebuddy.implementation.bind.annotation
)
must be of an array type and are assigned
an array containing all the source method's arguments. For this purpose, all source method parameters must
be assignable to the array's component type. If this is not the case, the current target method is not
considered as a candidate for being bound to the source method.
@This
annotation induces the assignment of the dynamic type's instance on which the
intercepted method is currently invoked. If the annotated parameter is not assignable to an instance of
the dynamic type, the current method is not considered as a candidate for being bound to the source method.
Note that calling any methods on this instance will result in calling a potentially instrumented method.
For calling the original implementations, you need to use the @Super
annotation
which is discussed below. A typical reason for using the @This
annotation is to gain access to an
instance's fields.
@Origin
must be declared using one of the following types:
Method
, Constructor
, Class
, Executable
,
MethodHandle
, MethodType
, String
or int
.
Depending on the parameter's type, it is assigned a different value to it, as described in the table below.
Parameter type | Assigned value |
---|---|
Method , Constructor or Class |
Reference to the original method or constructor (that is now instrumented) or the dynamically created class. |
Executable |
A method or constructor reference (requires Java 8+). |
String |
The value that the Method#toString() would have returned. |
MethodHandle |
An object that enables calling the original method. |
MethodType | An object that represents the signature of the MethodHandle . |
int | The modifier of the instrumented method. |
String
values as method identifiers wherever possible and discourage the use of
Method
objects as their lookup introduces a significant runtime overhead. To avoid this overhead,
the @Origin
annotation also offers a property for caching such instances for reuse. Note that
the MethodHandle
and MethodType
are stored in a class's constant pool such that
classes using these constants must at least be of Java version 7. Instead of using reflection for reflectively
invoking an intercepted method on another object, we furthermore recommend the use of the @Pipe
annotation which is discussed later in this section.
Byte Buddy also allows you to define your own annotations by registering
one or several ParameterBinder
s. We will look into such customization in the last section of this
tutorial.
Besides the four annotation we have discussed so far, there exist two other predefined annotations that grant
access to the super implementations of a dynamic type's methods. This way, a dynamic type could for example add
aspects to a class such as the logging
of method invocations. Using the @SuperCall
annotation, an invocation of the super implementation of
a method can be executed even from outside the dynamic class as demonstrated in the following example:
class MemoryDatabase { public List<String> load(String info) { return Arrays.asList(info + ": foo", info + ": bar"); } } class LoggerInterceptor { public static List<String> log(@SuperCall Callable<List<String>> zuper) throws Exception { System.out.println("Calling database"); try { return zuper.call(); } finally { System.out.println("Returned from database"); } } } MemoryDatabase loggingDatabase = new ByteBuddy() .subclass(MemoryDatabase.class) .method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class)) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance();
From the above example, it is obvious that the super method is called by injecting some instance of a
Callable
into the LoggerInterceptor
, which invokes the original non-overridden
implementation of MemoryDatabase#load(String)
from its call
method.
Byte Buddy creates a helper class that implements Callable
.
This class is called an AuxiliaryType
within Byte Buddy's terminology. Auxiliary types are created
on demand by Byte Buddy and are directly accessible from the DynamicType
interface after a
class was created. Because of such auxiliary types, the manual creation of one dynamic type might
result in the creation of several additional types which aid the implementation of the original class. Finally,
note that the @SuperCall
annotation can also be used on the Runnable
type where
the original method's return value is however dropped.
You might still wonder how this auxiliary type is able to call a super method of another type, which is normally forbidden in Java. On closer inspection, this behavior is however quite common and resembles the compiled code that is generated when the following Java source code snippet gets compiled:
class LoggingMemoryDatabase extends MemoryDatabase { private class LoadMethodSuperCall implements Callable { private final String info; private LoadMethodSuperCall(String info) { this.info = info; } @Override public Object call() throws Exception { return LoggingMemoryDatabase.super.load(info); } } @Override public List<String> load(String info) { return LoggerInterceptor.log(new LoadMethodSuperCall(info)); } }
Sometimes, you might however want to call a super method with different arguments than those that were assigned on
the method's original invocation. This is also possible in Byte Buddy by using the @Super
annotation.
This annotation triggers the creation of another AuxiliaryType
which now extends a super class or
an interface of the dynamic type in question. Similar to before, the auxiliary type overrides all methods to
call their super implementations on the dynamic type. This way, the logger interceptor from the previous
example can be implemented to change the actual invocation:
class ChangingLoggerInterceptor { public static List<String> log(String info, @Super MemoryDatabase zuper) { System.out.println("Calling database"); try { return zuper.load(info + " (logged access)"); } finally { System.out.println("Returned from database"); } } }
Note that the instance that is assigned to the parameter annotated with @Super
is of a different
identity to the actual instance of the dynamic type! Therefore, no instance field that is accessible by means of
the parameter reflects the actual instance's field. Furthermore, non-overridable methods of the auxiliary instance
do not delegate their invocations. They retain the original implementation that can result in absurd behavior when
they are invoked. Finally, in case a parameter annotated with @Super
does not represent
a super type of the relevant dynamic type, the method is not considered as a binding target for any of its
methods.
Because the @Super
annotation allows the use of any type, we might be required to provide
information on how this type can be constructed. By default, Byte Buddy attempts to use a class's default
constructor. This always works for interfaces which implicitly extend the Object
type. However,
when extending a super class of the dynamic type, this class might not even provide a default constructor.
If this is the case or if a specific constructor should be used for creating such an auxiliary type, the
@Super
annotation allows to identify a different constructor by setting its parameter types as
the annotation's constructorParameters
property. This constructor will then be called by assigning
the corresponding default value to each parameter.
Alternatively, it is also possible to set the strategy
property to
Super.Instantiation.UNSAFE
for instantiating the auxiliary type using Java internal mechanisms,
which does not invoke any constructor. However, note that this strategy is not
necessarily portable to non-Oracle JVMs and might no longer be available in future JVM releases. As of today,
the internal classes that are used by this unsafe instantiation strategy are however found in almost any JVM
implementation.
You might already have noticed that the above LoggerInterceptor
declares a
checked
Exception
. On the other hand, the instrumented source method which invokes this method does
not declare any checked exception. Usually, the Java compiler would refuse to compile such an invocation.
However, in contrast to the compiler, the Java runtime does not treat checked exceptions differently than their
unchecked counterparts and permits this invocation. For this reason, we decided to ignore checked exceptions and
grant full flexibility in their use. However, be careful when throwing undeclared checked exceptions
from dynamically created methods since the encounter of such an exception might confuse the users of your
application.
There is another caveat in the method delegation model that might have come to your attention. While static typing is great for implementing methods, strict types can limit the reuse of code. To understand why, consider the following example:
class Loop { public String loop(String value) { return value; } public int loop(int value) { return value; } }
Because the methods of the above class describe two similar signatures with incompatible types, you would not
usually be able to instrument both methods by using a single interceptor method. Instead, you would have to provide
two different target methods with different signatures only to satisfy the static type check. To overcome this
limitation, Byte Buddy allows to annotate methods and method parameters with @RuntimeType
, which
instructs Byte Buddy to suspend the strict type check in favor of a runtime type casting:
class Interceptor { @RuntimeType public static Object intercept(@RuntimeType Object value) { System.out.println("Invoked method with: " + value); return value; } }
Using the above target method, we are now able to provide a single interception method for both source methods.
Note that Byte Buddy is also able to box and unbox primitive values. However, be aware that the use of
@RuntimeType
comes at the cost of abandoning type safety, and you might end up with a
ClassCastException
if you get incompatible types mixed up.
As an equivalent to @SuperCall
, Byte Buddy comes with a @DefaultCall
annotation which
allows the invocation of a default method instead of calling a method's super method. A method with this parameter
annotation is only considered for binding if the intercepted method is, as a matter of fact, declared as a default
method by an interface that is directly implemented by the instrumented type. Similarly, a @SuperCall
annotation prevents a method's binding if the instrumented method does not define a non-abstract super method. If
you however want to invoke a default method on a specific type, you can specify the @DefaultCall
's
targetType
property with a specific interface. With this specification, Byte Buddy injects a proxy
instance which invokes the given interface type's default method, if such a method exists. Otherwise, the target
method with the parameter annotation is not considered as a delegation target. Obviously, default method invocation
is only available for classes that are defined in a class file version equal to Java 8 or newer. Similarly,
in addition to the @Super
annotation, there is a @Default
annotation which injects
a proxy for invoking a specific default method explicitly.
We already mentioned that you can define and register custom annotations with any MethodDelegation
.
Byte Buddy comes with one annotation that is ready for use, but still needs to be installed and registered
explicitly. By using the @Pipe
annotation, you can forward an intercepted method invocation to another
instance. The @Pipe
annotation is not preregistered with the MethodDelegation
because
the Java class library does not come with a suitable interface type before Java 8 which defines the
Function
type. Therefore, you need to explicitly provide a type with a single non-static method which
takes an Object
as its argument and returns another Object
as a result. Note that you
can still use a generic type as long as the method types are bound by the Object
type. Of course,
if you are using Java 8, the Function
type is a feasible option. When invoking the method on the
parameter's argument, Byte Buddy casts the parameter to the method's declaring type and invokes the intercepted
method with the same arguments as the original method call. Before we look at an example, let us however define
a custom type which you can use with Java {{javaVersion}} to 7 (since from Java 8+ you can use the Function
interface):
interface Forwarder<T, S> { T to(S target); }
Using this type, we can now implement a new solution to logging access of the above MemoryDatabase
by forwarding a method invocation to an existing instance:
class ForwardingLoggerInterceptor { private final MemoryDatabase memoryDatabase; // constructor omitted public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) { System.out.println("Calling database"); try { return pipe.to(memoryDatabase); } finally { System.out.println("Returned from database"); } } } MemoryDatabase loggingDatabase = new ByteBuddy() .subclass(MemoryDatabase.class) .method(named("load")).intercept(MethodDelegation.withDefaultConfiguration() .withBinders(Pipe.Binder.install(Forwarder.class)) .to(new ForwardingLoggerInterceptor(new MemoryDatabase()))) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance(); loggingDatabase.load("Hello");
In the above example, we only forward the invocation to another instance that we create locally. However, the advantage over intercepting a method by subclassing a type, is that this approach allows to enhance an already existing instance. Furthermore, you would normally register an interceptor on the instance level instead of registering a static interceptor on the class level.
So far, we have seen a great deal of the MethodDelegation
implementation. But before we proceed,
we want to take a more detailed look on how Byte Buddy selects a target method. We already described how Byte
Buddy resolves a most specific method by comparing parameter types, but there is more to it. After Byte Buddy
identified candidate methods that qualified for a binding to a given source method, it delegates the resolution
to a chain of AmbiguityResolver
s. Again, you are free to implement your own ambiguity resolvers that
can complement or even replace Byte Buddy's defaults. Without such alterations, the ambiguity resolver chain
attempts to identify a unique target method by applying the following rules in the same order as below:
@BindingPriority
. If a method
is of a higher priority than another method, the high priority method is always preferred over that with
lower priority. In addition, a method that is annotated by @IgnoreForBinding
is never
considered as a target method.
@Argument
, the method with
the most specific parameter types is considered. In this context, it does not matter if the annotation
is provided explicitly or implicitly by not annotating a parameter. The resolution algorithm works similar
to the Java compiler's algorithm for resolving calls to overloaded methods. If two types are equally
specific, the method that binds more arguments is considered as a target. If a parameter should be
assigned an argument without considering the parameter type at this resolution stage, this is possible
by setting the annotation's bindingMechanic
attribute to BindingMechanic.ANONYMOUS
.
Furthermore, note that non-anonymous parameters need to be unique per index value on each target method for the
resolution algorithm to work.
So far, we only delegated method invocations to static methods by naming a specific class as in
MethodDelegation.to(Target.class)
. It is however also possible to delegate to instance methods
or to constructors:
MethodDelegation.to(new Target())
, it is possible to delegate method invocations
to any of the instance methods of the Target
class. Note that this includes methods that
are defined anywhere in the instance's class hierarchy, including the methods that are defined in the
Object
class. You might want to filter the range of possible candidate methods
by calling MethodDelegation.withDefaultConfiguration().filter(ElementMatcher)
.
The ElementMatcher
type is the same that was used before for
selecting source methods within Byte Buddy's domain specific language. The instance which is the target of the
method delegation is stored in a static field. Similarly to the definition of fixed values, this requires the
definition of a TypeInitializer
. Instead of storing a delegation in a static field, you can
alternatively define the use of any field by MethodDelegation.toField(String)
where the argument specifies a field name to which all method delegations are forwarded. Always
remember to assign a value to this field before calling methods on an instance of such a dynamic class.
Otherwise, a method delegation will result in a NullPointerException
.
MethodDelegation.toConstructor(Class)
, any invocation of an intercepted method returns a
new instance of the given target type.
As you just learned, the MethodDelegation
inspects annotations for adjusting its binding logic.
These annotations are specific to Byte Buddy but this does not mean that the annotated classes become in any
way dependent on Byte Buddy. Instead, the Java runtime simply ignores annotation types that cannot be found on
the class path when a class is loaded. This implies that Byte Buddy is no longer required after a dynamic class
is created. This means that you could load the dynamic classes and the types to which it delegates its method calls
in another JVM process, even without having Byte Buddy on the class path.
There are several more predefined annotations that can be used with a MethodDelegation
that we
only want to name briefly. If you want to read more about these annotations, you can find further information in
the in-code documentation. These annotations are:
@Empty
: Applying this annotation, Byte Buddy injects the parameter type's default value. For
primitive types, this is the equivalent of the number zero, for reference types, this is null
.
Using this annotation is meant for voiding an interceptor's parameter.
@StubValue
: With this annotation, the annotated parameter is injected a stub value of the
intercepted method. For reference-return-types and void
methods, the value null
is injected. For methods that return a primitive value, the equivalent boxing type of 0
is
injected. This can be helpful in combination when defining a generic interceptor that returns
a Object
type while using a @RuntimeType
annotation. By returning the injected
value, the method behaves as a stub while correctly considering primitive return types.
@FieldValue
: This annotation locates a field in the instrumented type's class hierarchy and
injects the field's value into the annotated parameter. If no visible field of a compatible type can be
found for the annotated parameter, the target method is not bound.
@FieldProxy
: Using this annotation, Byte Buddy injects an accessor for a given field. The accessed
field can either be specified explicitly by its name or it is derived from a getter or setter methods name,
in case that the intercepted method represents such a method. Before this annotation can be used, it needs
to be installed and registered explicitly, similarly to the @Pipe
annotation.
@Morph
: This annotation works very similar to the @SuperCall
annotation. However,
using this annotation allows to specify the arguments that should be used for invoking the super method.
Note that you should only use this annotation when you need to invoke a super method with different arguments
than the original invocation, since using the @Morph
annotation requires a boxing and unboxing of
all arguments. If you want to invoke a specific super method, consider using the @Super
annotation for creating a type-safe proxy. Before this annotation can be used, it needs to be installed and
registered explicitly, similarly to the @Pipe
annotation.
@SuperMethod
: This annotation can only be used on parameter types that are assignable from
Method
. The assigned method is set to be a synthetic accessor method that allows for the invocation
of the original code. Note that using this annotation causes a public accessor to be created for the proxy class
that allows for the outside invocation of the super method without passing a security manager.
@DefaultMethod
: Similar to @SuperMethod
but for a default method call. The default
method is invoked on a unique type if there is only one possibility for a default method invocation. Otherwise,
a type can be specified explicitly as an annotation property.
As the name suggests, the SuperMethodCall
implementation (do not confuse this class with the @SuperCall
annotation)
can be used to invoke a method's super
implementation. At first glance, the sole invocation of a super implementation does not seem very useful
since this will not change an implementation but only replicate existent logic. However, by overriding a method,
you are able to change the annotations of a method and its parameters, something we will look into in the next
section. Another rationale for calling a super method in Java is however the definition of a constructor which must
always invoke another constructor of either its super type or its own type.
So far we simply assumed that the constructors of a dynamic type would always resemble the constructors of its direct super type. As an example, we could call
new ByteBuddy() .subclass(Object.class) .make()
to create a subclass of Object
with a single default constructor which is defined to simply invoke its
direct super constructor, the default constructor of Object
. However, this behavior is not stipulated
by Byte Buddy. Instead, the above code is a shortcut for calling
new ByteBuddy() .subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS) .make()
where a ConstructorStrategy
is responsible for creating a set of predefined constructors for any given
class. Besides the above strategy, which copies each visible constructor of a dynamic type's direct super class,
there exist three other predefined strategies: (i) one that does not create any constructor at all,
(ii) one that creates a default constructor, which is invoking the direct super class's default constructor
and throws an exception if no such constructor exists, and (iii) one that only imitates public constructors of the super type.
Within the Java class file format, constructors do not generally differ from methods such that Byte Buddy allows
them to be treated just as such. However, constructors are required to contain a hard-coded invocation of another
constructor to be accepted by the Java runtime. For this reason, most predefined implementations besides
SuperMethodCall
will fail to create a valid Java class when applied to a constructor.
However, by using custom implementations, you are able to define your own constructors by either implementing a
custom ConstructorStrategy
or by defining an individual constructor within Byte Buddy's domain specific
language using the defineConstructor
method.
For class rebasing and class redefinition, constructors are of course simply retained, which makes the specification
of a ConstructorStrategy
obsolete. Instead, for copying these retained constructors' (and methods')
implementations, it is required to specify a ClassFileLocator
which allows a lookup of the original
class file that contains these constructor definitions. Byte Buddy does its best to identify the location of the
original class file by itself, e.g. by querying the corresponding ClassLoader
or by looking on an
application's class path. When dealing with customary class loader, a lookup might however still not be successful.
Then, a custom ClassFileLocator
can be provided.
With its version 8 release, the Java programming language introduced
default methods for interfaces.
In Java, a default method invocation is expressed by a similar syntax to the invocation of a super method. As an
only disparity, a default method invocation names the interface that defines the method. This is necessary because
a default method invocation can be ambiguous if two interfaces define a method with identical signature.
Accordingly, Byte Buddy's DefaultMethodCall
implementation (do not confuse this class with the @DefaultCall
annotation) takes a list of prioritized
interfaces. When intercepting a method, the DefaultMethodCall
invokes a default method on the
first-mentioned interface. As an example, assume that we wanted to implement the two following interfaces:
interface First { default String qux() { return "FOO"; } } interface Second { default String qux() { return "BAR"; } }
If we now created a class that implements both interfaces and implemented the qux
method to call a
default method, this invocation could express both a call of the default method defined on the First
or the Second
interface. However, by specifying the DefaultMethodCall
to prioritize the
First
interface, Byte Buddy would know that it should invoke this latter interface's method instead of
the alternative.
new ByteBuddy(ClassFileVersion.JAVA_V8) .subclass(Object.class) .implement(First.class) .implement(Second.class) .method(named("qux")).intercept(DefaultMethodCall.prioritize(First.class)) .make()
Note that any Java class that is defined in a class file version before Java 8 does not support default methods.
Furthermore, you should be aware that Byte Buddy imposes weaker requirements on the invokability of a default
method compared to the Java programming language. Byte Buddy only requires a default method's interface to be
implemented by the most-specific class in a type's hierarchy. Other than the Java programming language, it does not
require this interface to be the most specific interface that is implemented by any super class. Finally, if you do
not expect an ambiguous default method definitions, you can always use
DefaultMethodCall.unambiguousOnly()
for receiving an implementation which throws an exception on the
discovery of an ambiguous default method invocation. This same behavior is displayed by a prioritizing
DefaultMethodCall
where a default method call is ambiguous between non-prioritized interfaces and no
prioritized interface was found to define a method with a compatible signature.
In some cases, the above Implementation
s are not sufficient to implement more custom behavior. For
example, one might want to implement a custom class with explicit behavior. For example, we might want to implement
the following Java class with a constructor that does not have a super constructor with identical arguments:
public class SampleClass { public SampleClass(int unusedValue) { super(); } }
The previous SuperMethodCall
implementation could not be used to implement this class as the
Object
class does not define a constructor that takes an int
as its parameter. Instead,
we can invoke the Object
super constructor explicitly:
new ByteBuddy() .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS) .defineConstructor(Visibility.PUBLIC) .withParameters(int.class) .intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())) .make();
With the above code, we have created a simple subclass of Object
that defines a single constructor
which takes a single int
parameter that is not used. The latter constructor is then implemented by
an explicit method call to the Object
super constructor.
The MethodCall
implementation can also be used when passing arguments. These arguments are either
passed explicitly as a value, as a value for an instance field that needs to be set manually or as a given
parameter value. Also, the implementation allows to invoke methods on other instances than the one being
instrumented. Furthermore, it allows for the construction of new instances to be returned from an
intercepted method. The documentation of the MethodCall
class provides detailed information on these
features.
Using the FieldAccessor
, it is possible to implement a method to read or to write a field value. In
order to be compatible to this implementation, a method must either:
void setBar(Foo f)
to define a field setter. The setter will normally
access a field named bar
, as it is conventional in the
Java bean
specification. In this context, the parameter type Foo
must be a subtype of this field's
type.
Foo getBar()
to define a field getter. The getter will normally
access a field named bar
, as it is conventional in the Java bean specification. For this to be
possible, the method's return type Foo
must be a super type of the field's type.
Creating such an implementation is trivial: Simply call FieldAccessor.ofBeanProperty()
. However,
if you do not want to derive a field's name from a method's name, you can still specify the field name explicitly
by using FieldAccessor.ofField(String)
. Using this method, the only argument defines the field's name
that should be accessed. If required, this even allows you to define a new field if such a field does not yet
exist. When accessing an existing field, you are able to specify the type in which a field is defined by calling
the in
method. In Java, it is legal to define a field in several classes of a hierarchy. In the
process, a field of a class is shadowed by the field definition in its subclass. Without such an explicit
location of the field's class, Byte Buddy will access the first field it encounters by traversing through a class
hierarchy, starting with the most specific class.
Let us look at an example application of the FieldAccessor
. For this example, we assume that we
receive some UserType
that we want to subclass at runtime. For this purpose, we want to register an
Interceptor
for each instance which is represented by an interface. This way, we are able to provide
different implementations according to our actual requirement. This latter implementation should then be
exchangeable by calling methods of the InterceptionAccessor
interface on the corresponding instance.
In order to create instances of this dynamic type, we further do not want to use reflection, but call a method of
an InstanceCreator
which serves as an object factory. The following types resemble this setup:
class UserType { public String doSomething() { return null; } } interface Interceptor { String doSomethingElse(); } interface InterceptionAccessor { Interceptor getInterceptor(); void setInterceptor(Interceptor interceptor); } interface InstanceCreator { Object makeInstance(); }
We already learned how to intercept methods of a class by using a MethodDelegation
. Using the latter
implementation, we can define a delegation to an instance field and name this field interceptor
.
Additionally, we are implementing the InterceptionAccessor
interface and intercept all methods
of the interface to implement accessors of this field. By defining a bean property accessor, we achieve a getter
for getInterceptor
and a setter for setInterceptor
:
Class<? extends UserType> dynamicUserType = new ByteBuddy() .subclass(UserType.class) .method(not(isDeclaredBy(Object.class))) .intercept(MethodDelegation.toField("interceptor")) .defineField("interceptor", Interceptor.class, Visibility.PRIVATE) .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty()) .make() .load(getClass().getClassLoader()) .getLoaded();
With the new dynamicUserType
, we can implement the InstanceCreator
interface to
become a factory of this dynamic type. Again, we are using the already known MethodDelegation
to
call the dynamic type's default constructor:
InstanceCreator factory = new ByteBuddy() .subclass(InstanceCreator.class) .method(not(isDeclaredBy(Object.class))) .intercept(MethodDelegation.toConstructor(dynamicUserType)) .make() .load(dynamicUserType.getClassLoader()) .getLoaded().newInstance();
Note that we need to use the dynamicUserType
's class loader to load the factory. Otherwise, this
type would not be visible to the factory when it is loaded.
With these two dynamic types, we can finally create a new instance of the dynamically enhanced
UserType
and define custom Interceptor
s for its instances. Let us conclude this example
by applying some HelloWorldInterceptor
to a freshly created instance. Note how we are now able to do
this without using any reflection, thanks to both the field accessor interface and the factory.
class HelloWorldInterceptor implements Interceptor { @Override public String doSomethingElse() { return "Hello World!"; } } UserType userType = (UserType) factory.makeInstance(); ((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());
Additionally, to the Implementation
s we discussed so far, Byte Buddy includes several other
implementations:
StubMethod
implements a method to simply return the method return type's default value without
any further action. This way, a method call can be silently suppressed. This approach can for example be used
to implement mock types. The default value of any primitive type is zero or the zero character respectively.
Methods that return a reference type return null
as their default.
ExceptionMethod
can be used to implement a method to only throw an exception. As mentioned
before, it is possible to throw checked exceptions from any method even if a method does not declare this
exception.
MethodCall
implementation allows to simply forward a method call to another instance
of the same type as the declaring type of an intercepted method. The same result can be achieved using a
MethodDelegation
. However, by MethodCall
a simpler delegation model is applied
which can cover use cases where no target method discovery is required.
InvocationHandlerAdapter
allows to use existing InvocationHandler
s from
the proxy classes
that ship with the Java Class Library.
InvokeDynamic
implementation allows to bind a method dynamically at runtime using
bootstrap methods
which are accessible from Java 7 onwards.
We just learned how Byte Buddy relies on annotations for providing some of its functionality. And Byte Buddy is by far not the only Java application with an annotation-based API. In order to integrate dynamically created types with such applications, Byte Buddy allows to define annotations for its created types and their members. Before looking into the details of assigning annotations to dynamically created types, let us look at an example of annotating a runtime class:
@Retention(RetentionPolicy.RUNTIME) @interface RuntimeDefinition { } class RuntimeDefinitionImpl implements RuntimeDefinition { @Override public Class<? extends Annotation> annotationType() { return RuntimeDefinition.class; } } new ByteBuddy() .subclass(Object.class) .annotateType(new RuntimeDefinitionImpl()) .make();
As insinuated by Java's @interface
keyword, annotations are internally represented as interface
types. As a consequence, annotations can be implemented by a Java class just like an ordinary interface. The
only difference to implementing an interface is an annotation's implicit annotationType
method
which determines the annotation type a class represents. The latter method usually returns the implemented
annotation type's class literal. Other than that, any annotation property is implemented as if it was an
interface method. However, note that an annotation's default values need to be repeated by an annotation
method's implementation.
Defining annotations for a dynamically created class can be particularly important when a class should serve
as a subclass proxy for another class. A subclass proxy is often used to implement
cross-cutting concerns, where the subclass
should mimic the original class as transparently as possible. However, annotations on a class are not retained
for its subclasses as long as this behavior is explicitly required by defining an annotation to be
@Inherited
.
Using Byte Buddy, creating subclass proxies that retain their base class's annotations is easy by invoking
the attribute
method of Byte Buddy's domain specific language. This method expects a
TypeAttributeAppender
as its argument. A type attribute appender offers a flexible way for defining
the annotations of a dynamically created class, based on its base class. For example, by passing a
TypeAttributeAppender.ForInstrumentedType
, a class's annotations are copied to its dynamically created
subclasses. Note that annotations and type attribute appenders are additive and no annotation type must be
defined more than once for any class.
Method and field annotations are defined similarly to type annotations which we just discussed. A method annotation can be defined as a conclusive statement in Byte Buddy's domain specific language for implementing a method. Likewise, a field can be annotated after its definition. Again, let us look at an example:
new ByteBuddy() .subclass(Object.class) .annotateType(new RuntimeDefinitionImpl()) .method(named("toString")) .intercept(SuperMethodCall.INSTANCE) .annotateMethod(new RuntimeDefinitionImpl()) .defineField("foo", Object.class) .annotateField(new RuntimeDefinitionImpl())
The above code example overrides the toString
method and annotates the overridden method with
RuntimeDefinition
. Furthermore, the created type defines a field foo
that carries the
same annotation and also defines the latter annotation on the created type itself.
By default, a ByteBuddy
configuration does not predefine any annotations for a dynamically created
type or type member. However, this behavior can be altered by providing a default
TypeAttributeAppender
, MethodAttributeAppender
or FieldAttributeAppender
.
Note that such default appenders are not additive but replace their former values.
Sometimes, it is desirable to not load an annotation type or the types of any of its properties when defining
a class. For this purpose, it is possible to use the AnnotationDescription.Builder
(as a parameter to annotateType()
, annotateMethod()
or annotateField()
methods),
which offers a fluent interface for defining an annotation without triggering class loading, but at the costs
of type safety. All annotation properties are however evaluated at runtime.
By default, Byte Buddy includes any property of an annotation into a class file, including default properties
that are specified implicitly by a default
value. This behavior can however be customized by
providing an AnnotationRetention
to a ByteBuddy
instance, by calling for instance
new ByteBuddy().with(AnnotationRetention.DISABLED)
.
Byte Buddy exposes and writes type annotations as they were introduced as a part of Java 8.
Type annotations are accessible as declared annotations by any TypeDescription.Generic
instance.
If a type annotation should be added to a type of a generic field or method, an annotated type can be generated using a
TypeDescription.Generic.Builder
.
A Java class file can include any custom information as a so-called attribute. Such attributes can be included by using
a Byte Buddy's *AttributeAppender
instance for a type, field or method. Attribute appenders can however
also be used for defining methods based on information that is provided by the intercepted type, field or method. For
example, it is possible to copy all annotations of an intercepted method when overriding a method in a subclass:
class AnnotatedMethod { @SomeAnnotation void bar() { } } new ByteBuddy() .subclass(AnnotatedMethod.class) .method(named("bar")) .intercept(StubMethod.INSTANCE) .attribute(MethodAttributeAppender.ForInstrumentedMethod.INCLUDING_RECEIVER)
The above code overrides the bar
method of the AnnotatedMethod
class but copies all
annotations of the overridden method, including annotations on parameters or types.
When a class is redefined or rebased, the same rule might not apply. By default, ByteBuddy
is
configured to preserve any annotations of a rebased or redefined method, even if the method is intercepted
as above. This behavior can however be changed such that Byte Buddy discards any preexisting annotations by
setting the AnnotationRetention
strategy to DISABLED
.
In the previous sections, we described Byte Buddy's standard API. None of the features described so far requires knowledge or the explicit expression of Java byte code. However, if you need to create custom byte code, you can do so by directly accessing the API of ASM, a low-level byte code library on top of which Byte Buddy is built. However, note that different versions of ASM are not compatible to another such that you need to repackage Byte Buddy into your own namespace when releasing your code. Otherwise, your application might introduce incompatibilities to other uses of Byte Buddy when another dependency is expecting a different version of Byte Buddy which is based on a different version of ASM. You can find detailed information on maintaining a dependency on Byte Buddy on the front page.
The ASM library comes with an excellent documentation of Java byte code and on the use of the library. Therefore, we want to refer you to this documentation in case that you want to learn in detail about Java byte code and ASM's API. Instead, we are only going to provide a brief introduction to the JVM's execution model and Byte Buddy's adaption of ASM's API.
Any Java class file is constituted by several segments. The core segments can be categorized roughly as follows:
Fortunately, the ASM library takes full responsibility of establishing an applicable constant pool when creating a
class. With this, the only non-trivial element remains the description of a method's implementation which is
represented by an array of execution instructions, each one encoded as a single byte. These instructions are processed
by a virtual stack machine on the method's invocation. For
a simple example, let us consider a method that calculates and returns the sum of the two primitive integers
10
and 50
. This method's Java byte code would look as follows:
LDC 10 // stack contains 10 LDC 50 // stack contains 10, 50 IADD // stack contains 60 IRETURN // stack is empty
The above mnemonic of an array of
Java byte code starts off by pushing both numbers onto the stack by using the LDC
instruction.
Note how this execution order differs from the order that is expressed in Java source code where the addition would
be written as the infix notation 10 + 50
. However, the latter order cannot be processed by a stack
machine where any instruction like +
can only access the uppermost values that are currently found on
the stack. This addition is expressed by IADD
, which consumes the two upmost stack values that are
expected to be primitive integers. In the process, it adds these two values and pushes the result back onto the top
of the stack. Finally, the IRETURN
statement consumes this calculation result and returns it from the
method, leaving us with an empty stack.
We already mentioned that any primitive value that is referenced in a method is stored in the class's constant
pool. This is also true for the numbers 50
and 10
which are referenced in the above
method. Any value in the constant pool is assigned an index with the length of two bytes. Let us assume
that the numbers 10
and 50
were stored at the indexes 1
and 2
.
Together with the byte values of the above mnemonic which are 0x12
for LDC
,
0x60
for IADD
and 0xAC
for IRETURN
, we now know how express
the above method as raw byte instructions:
12 00 01 12 00 02 60 AC
For a compiled class, this exact byte sequence could be found in the class file. However, this description does
not yet suffice to fully define a method's implementation. In order to accelerate a Java application's
runtime execution, each method is required to inform the Java virtual machine about the required size for its
execution stack. For the above method which comes without branches, this is rather easy to determine as we already
saw that there will be at most two values on the stack. However, for more complex methods, providing this
information can easily become a complex task. And to make things worse, stack values can be of different size.
Both long
and double
values consume two slots while any other value consumes one.
As if this wasn't enough, the Java virtual machine also requires information about the size of all local variables
within a method's body. All such variables in a method are stored in an array which also includes any method
parameter and the this
reference for non-static methods. Again, long
and
double
values consume two slots in this array.
Evidently, keeping track of all this information makes the manual assembly of Java byte code tedious and
error-prone which is why Byte Buddy provides a simplifying abstraction. Within Byte Buddy, any stack instruction is
contained by an implementation of the StackManipulation
interface. Any implementation of a stack
manipulation combines an instruction to alter a given stack and information on the size impact of this instruction.
Any number of such instructions can then easily be conflated to a common instruction. To demonstrate this, let us
first implement a StackManipulation
for the IADD
instruction:
enum IntegerSum implements StackManipulation { INSTANCE; // singleton @Override public boolean isValid() { return true; } @Override public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) { methodVisitor.visitInsn(Opcodes.IADD); return new Size(-1, 0); } }
From the above apply
method, we learn that this stack manipulation executes the IADD
instruction by invoking the related method on ASM's method visitor. Furthermore, the method expresses that the
instruction reduces the current stack Size
by one slot. The second argument of the created
Size
instance is 0
, which expresses that this instruction does not require a specific
minimal stack size for calculating interim results. Furthermore, any StackManipulation
can express to
be invalid. This behavior can be used for more complex stack manipulation, such as object assignments which
might break a type constraint. We will look at an example of an invalid stack manipulation later in this section.
Finally, note that we describe the stack manipulation as a
singleton enumeration. Using such
immutable, functional descriptions of stack manipulations proved to be a good practice for Byte Buddy's internal
implementation and we can only recommend you to follow the same approach.
By combining the above IntegerSum
with the predefined IntegerConstant
and the
MethodReturn
stack manipulations, we can now implement a method. Within Byte Buddy, a method
implementation is contained by a ByteCodeAppender
which we implement as follows:
enum SumMethod implements ByteCodeAppender { INSTANCE; // singleton @Override public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext, MethodDescription instrumentedMethod) { if (!instrumentedMethod.getReturnType().asErasure().represents(int.class)) { throw new IllegalArgumentException(instrumentedMethod + " must return int"); } StackManipulation.Size operandStackSize = new StackManipulation.Compound( IntegerConstant.forValue(10), IntegerConstant.forValue(50), IntegerSum.INSTANCE, MethodReturn.INTEGER ).apply(methodVisitor, implementationContext); return new Size(operandStackSize.getMaximalSize(), instrumentedMethod.getStackSize()); } }
Again, the custom ByteCodeAppender
is implemented as a singleton enumeration.
Before implementing the desired method, we first validate that the instrumented method really returns a primitive
integer. Otherwise, the created class would be rejected by the JVM's validator. Then we load the two numbers
10
and 50
onto the execution stack, apply the summation of these values and return the
calculation result. By wrapping all these instructions with a compound stack manipulation, we can conclusively
retrieve the aggregated stack size that is required to perform this chain of stack manipulations. Finally, we
return the overall size requirements of this method. The first argument of the returned
ByteCodeAppender.Size
reflects the required size for the execution stack, which we just mentioned to
be contained by the StackManipulation.Size
. Additionally, the second argument reflects the required
size for the local variable array. Here it simply resembles the required size for the method's parameters and
a possible this
reference, since we did not define any local variables of our own.
With this implementation of our summation method, we are now ready to write a custom Implementation
for this method which we can provide to Byte Buddy's domain specific language:
enum SumImplementation implements Implementation { INSTANCE; // singleton @Override public InstrumentedType prepare(InstrumentedType instrumentedType) { return instrumentedType; } @Override public ByteCodeAppender appender(Target implementationTarget) { return SumMethod.INSTANCE; } }
Any Implementation
is queried in two stages. First, an implementation gets the chance to
alter the created class by adding additional fields or methods in the prepare
method. Furthermore,
the preparation allows an implementation to register a TypeInitializer
which we learned about in
a previous section. If no such preparations are required, it suffices to return the unaltered
InstrumentedType
which is provided as the argument. Note that an Implementation
should
not normally return an individual instance of an instrumented type, but call the instrumented type's appender
methods which are all prefixed by with
. After any Implementation
for a particular class
creation is prepared, the appender
method is invoked for retrieving a ByteCodeAppender
.
This appender is then queried for any method that was selected for interception by the given implementation and
also for any method that was registered during the implementation's invocation of the prepare
method.
Note that Byte Buddy only invokes each Implementation
's prepare
and appender
methods a single time, during the creation process of any class. This is guaranteed, no matter how many times an
implementation is registered for use in a class's creation. This way, an Implementation
can avoid to
verify if a field or method is already defined. In the process, Byte Buddy compares Implementation
s
instances by their hashCode
and equals
methods. In general, any class that is used by
Byte Buddy should provide meaningful implementations of these methods. The fact that enumerations come with such
implementations per definition is another good reason for their use.
With all this, let us see the SumImplementation
in action:
abstract class SumExample { public abstract int calculate(); } new ByteBuddy() .subclass(SumExample.class) .method(named("calculate")) .intercept(SumImplementation.INSTANCE) .make()
Congratulations! You just extended Byte Buddy to implement a custom method that computes and returns the sum of
10
and 50
. Of course, this example implementation is not of much practical use. However,
more complex implementations can be implemented easily on top of this infrastructure. After all, if you feel that
you created something handy, please consider to contribute your implementation. We are
looking forward to hearing from you!
Before we move on to customizing some other components of Byte Buddy, we should briefly discuss the use of jump
instructions and the matter of the so-called Java stack frames. Since Java 6, any jump instruction, which are used
to implement statements such as if
or while
, require some additional
information in order to accelerate the JVM's verification process. This additional information is called a
stack map frame. A stack map frame contains information about all values that are found on the execution
stack at any target of a jump instruction. By providing this information, the JVM's verifier saves some work,
which is now however left to us.
For more complex jump instructions, providing correct stack map frames is a rather difficult task and many code generation frameworks have quite some trouble to always create correct stack map frames. So, how do we deal with this matter? As a matter of fact, we simply don't. It is Byte Buddy's philosophy that code generation should only be used as the glue between a type hierarchy that is unknown at compile time and custom code that needs to be injected into these types. The actual code that is generated should therefore remain as confined as possible.
Wherever possible, conditional statements should rather be implemented and compiled in a JVM language of your choice and then be bound to a given method by using a minimalistic implementation. A nice side effect of this approach is that Byte Buddy's users can work with normal Java code and use their accustomed tools like debuggers or IDE code navigators. None of this would be possible with generated code which does not have a source code representation. However, if you really need to create byte code with jump instructions, make sure to add the correct stack map frames using ASM, since Byte Buddy will not automatically include them for you.
In a previous section, we discussed that Byte Buddy's built in Implementation
s rely on an
Assigner
in order to assign values to variables. In this process, an Assigner
is able to
apply a transformation of one value to another, by emitting an appropriate StackManipulation
. Doing so,
Byte Buddy's built-in assigners provide, for example, auto-boxing of primitive values and their wrapper types.
In the most trivial case, a value is assignable to a variable as is. In some cases, an assignment may
however not be possible at all, which can be expressed by returning an invalid StackManipulation
from an
assigner. A canonical implementation of an invalid assignment is provided by Byte Buddy's
IllegalStackManipulation
class.
To demonstrate the use of a custom assigner, we are now going to implement an Assigner
that only
assigns values to String
-typed variables by calling the toString
method on any
value it receives:
enum ToStringAssigner implements Assigner { INSTANCE; // singleton @Override public StackManipulation assign(TypeDescription.Generic source, TypeDescription.Generic target, Assigner.Typing typing) { if (!source.isPrimitive() && target.represents(String.class)) { MethodDescription toStringMethod = new TypeDescription.ForLoadedType(Object.class) .getDeclaredMethods() .filter(named("toString")) .getOnly(); return MethodInvocation.invoke(toStringMethod).virtual(sourceType.asErasure()); } return StackManipulation.Illegal.INSTANCE; } }
The above implementation first validates that the input value is not of a primitive type and that the target
variable type is of a String
type. If these conditions are not fulfilled, the Assigner
emits an IllegalStackManipulation
to render the attempted assignment invalid. Otherwise, we identify
the Object
type's toString
method by its name. We then use Byte Buddy's
MethodInvocation
to create a StackManipulation
that calls this method virtually on the
source type. Finally, we can integrate this custom Assigner
with, for example, Byte Buddy's
FixedValue
implementation as follows:
new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(FixedValue.value(42) .withAssigner(new PrimitiveTypeAwareAssigner(ToStringAssigner.INSTANCE), Assigner.Typing.STATIC)) .make()
When the toString
method is called on an instance of the above type, it will return the string value
42
. This is only possible by using our custom assigner which converts the Integer
type
to a String
by invoking the toString
method. Note that we additionally wrapped
the custom assigner with the built-in PrimitiveTypeAwareAssigner
which performs an auto-boxing of
the provided primitive int
to its wrapper type before delegating the assignment of this wrapped
primitive value to its inner assigner. Other built-in assigners are the VoidAwareAssigner
and the
ReferenceTypeAwareAssigner
. Always remember to implement meaningful hashCode
and
equals
methods for your custom assigners since those methods are normally called from their
counterparts in the Implementation
that is making use of a given assigner. Again, by implementing
an assigner as a singleton enumeration, we avoid doing this manually.
We already mentioned in a previous section that it is possible to extend the MethodDelegation
implementation to process user-defined annotations. For this purpose, we need to provide a custom
ParameterBinder
which knows how to handle a given annotation. As an example, we want to define
an annotation with the purpose to simply inject a fixed string into the annotated parameter. First, we define such
a StringValue
annotation:
@Retention(RetentionPolicy.RUNTIME) @interface StringValue { String value(); }
We need to make sure that the annotation is visible at runtime by setting the appropriate
RuntimePolicy
. Otherwise, the annotation is not retained at runtime and Byte Buddy does not have a
chance to discover it. Doing so, the above value
property contains the string which is assigned to
the annotated parameter as a value.
With our custom annotation, we need to create a corresponding ParameterBinder
which is able to create
a StackManipulation
which expresses the binding for this parameter. This parameter binder is invoked,
by the MethodDelegation
, each time its corresponding annotation is discovered on a parameter.
Implementing a custom parameter binder for our example annotation is straight forward:
enum StringValueBinder implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> { INSTANCE; // singleton @Override public Class<StringValue> getHandledType() { return StringValue.class; } @Override public MethodDelegationBinder.ParameterBinding<?> bind( AnnotationDescription.Loadable<StringValue> annotation, MethodDescription source, ParameterDescription target, Implementation.Target implementationTarget, Assigner assigner, Assigner.Typing typing) { if (!target.getType().asErasure().represents(String.class)) { throw new IllegalStateException(target + " makes illegal use of @StringValue"); } StackManipulation constant = new TextConstant(annotation.load().value()); return new MethodDelegationBinder.ParameterBinding.Anonymous(constant); } }
Initially, the parameter binder makes sure that the target
parameter is actually a
String
type. If this is not the case, we will throw an exception to inform the annotation's user
of his illegal placing of this annotation. Otherwise, we simply create a TextConstant
which represents
the loading of a constant pool string onto the execution stack. This StackManipulation
is then wrapped
as an anonymous ParameterBinding
, which is finally returned from the method. Alternatively, you could
have provided either a Unique
or an Illegal
parameter binding. A unique binding is
identified by any object which allows to retrieve this binding from an AmbiguityResolver
. In a later
step, such a resolver is able to look up if a parameter binding was registered with some unique identifier,
then it can decide if this binding is superior to another successfully bound method. With an illegal binding, one can
instruct Byte Buddy that a specific pair of a source
and a target
method is incompatible
and cannot be bound together.
This already is all the information that is required for using a custom annotation with a
MethodDelegation
implementation. After receiving a ParameterBinding
, it makes sure that
its value is bound to the correct parameter, or it will discard the current pair of a source
and
target
method as unbindable. Furthermore, it will allow AmbiguityResolver
s to look up
unique bindings. Finally, let us put this custom annotation in action:
class ToStringInterceptor { public static String makeString(@StringValue("Hello!") String value) { return value; } } new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(MethodDelegation.withDefaultConfiguration() .withBinders(StringValueBinder.INSTANCE) .to(ToStringInterceptor.class)) .make()
Note that by specifying the StringValueBinder
as the only parameter binder, we replace all defaults.
Alternatively, we could have appended the parameter binder to those that are already registered. With only one
possible target method in the ToStringInterceptor
, the dynamic class's intercepted
toString
method is bound to the makeString
invocation. When the target method is invoked,
Byte Buddy assigns the annotation's string value as the target method's only parameter.