Why do you need to generate code at run time?

Java is a strongly typed language system, which requires variables and objects have a certain type, incompatible type assignment will cause abnormal conversion, normally this kind of mistake will be compiler to check out, such a strict type in most cases is relatively satisfactory, the stability of the building has a very strong readability and application has a lot of help, This is one of the reasons Java has become ubiquitous in enterprise programming. However, because of strong type checking, other domain languages are limited. For example, when writing a framework, we usually don’t know the types defined by the application because we don’t know them when the library is compiled. In this case, the Java class library provides a reflection API to call or access the application’s methods or variables. Using the reflection API, we can reflect on known types to invoke methods or access properties. However, Java reflection has the following disadvantages:

  • A fairly expensive method lookup is performed to get an object that describes a particular method, so using the reflection API is very slow compared to hard-coded method calls.
  • Reflection apis can bypass type safety checks and can cause unexpected problems when used improperly, missing out on one of the features of the Java programming language.

Introduction to the

Byte Buddy is a code generation and manipulation library for creating and modifying Java classes while Java applications are running, without the help of a compiler. In addition to the code generation utility that comes with the Java class library, Byte Buddy allows you to create any class and is not limited to implementing an interface for creating run-time proxies. In addition, Byte Buddy provides a convenient API to use Java proxies or manually change classes during the build process. Byte Buddy has the following advantages over other bytecode manipulation libraries:

  • You don’t need to understand the bytecode format to manipulate it, and a simple API makes it easy to manipulate bytecode.
  • Any version of Java is supported, the library is lightweight and depends solely on the Visitor API of ASM, the Java byte code parser library, which itself does not require any additional dependencies.
  • Byte Buddy offers performance advantages over JDK dynamic proxies, Cglib, and Javassist.

performance

When selecting a bytecode manipulation library, you often need to consider the performance of the library itself. For many applications, the runtime nature of the generated code is more likely to determine the best choice. Besides the runtime of the generated code itself, the runtime used to create dynamic classes is also an issue. The official website carries out the performance test of the library, and gives the following results:

Each row in the diagram is the result of class creation, interface implementation, method call, type extension, and superclass method call. As you can see from the performance report, Byte Buddy’s main focus is on generating code with minimal run-time. It’s important to note that our tests to measure Java code performance are optimized by the Java virtual machine just-in-time compiler. If your code is run only occasionally and not optimized by the virtual machine, Performance may vary. So when we were developing with Byte Buddy, we wanted to monitor these metrics to avoid a performance penalty as we added new features.

Hello world!

Class<? > dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(FixedValue.value("Hello World")) .make() .load(HelloWorldBuddy.class.getClassLoader()) .getLoaded(); Object instance = dynamicType.newInstance(); String toString = instance.toString(); System.out.println(toString); System.out.println(instance.getClass().getCanonicalName());Copy the code

As you can see from the example, creating a class is so simple. As ByteBuddy explains, ByteBuddy provides a domain-specific language that makes human readability as easy as possible with simple apis that might allow you to code your first use without having to refer to the API. That’s one reason ByteBuddy has exploded in popularity with other libraries of the same type.

The default ByteBuddy configuration used in the above example creates Java classes in the latest version of the class file format that can be understood by the Java VIRTUAL machine being processed. Subclass specifies the parent class of the newly created class, method specifies the toString method of Object, Intercept intercepts the toString method and returns a fixed value, and finally make produces bytecode. A class loader is loaded into the virtual machine.

Additionally, Byte Buddy is not limited to creating subclasses and action classes, but can also transform existing code. Byte Buddy also provides a convenient API for defining so-called Java proxies, which allow code conversion during the run of any Java application, and will be covered in a separate article in the next article.

Create a class

Any type created by ByteBuddy is done through an instance of the ByteBuddy class. A new instance can be created by simply calling new ByteBuddy().

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

The above example code creates a class that inherits from type Object. This dynamically created type is equivalent to a type that extends Object directly and does not implement any methods, properties, or constructors. This column does not have a named dynamically generated type, but it is required when defining Java classes, so it is easy to imagine that ByteBuddy would have a default policy for us to generate. Of course, you can just as easily name the type explicitly.

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

So what does the default strategy do? This will have to do with ByteBuddy and convention over configuration, which provides what we think is a fairly comprehensive default configuration. As for type naming, ByteBuddy’s default configuration provides NamingStrategy, which randomly generates class names based on dynamically typed superclass names. In addition, the name is defined under the same package as the parent class, so that the parent class’s package-level access methods are also visible to dynamic types. If you named example subclasses example Foo, so the name of the generated will be similar to the example. The FooByteBuddy1376491271, the sequence of Numbers here is random.

In addition, in some need to specify the type of scenario, can be done by rewriting NamingStrategy method, or use ByteBuddy built-in NamingStrategy. SuffixingRandom to implement.

It is also important to note that we need to code in accordance with so-called domain-specific languages and principles of immutability. What does that mean? That is, in ByteBuddy, almost all classes are built immutable; In rare cases, it is impossible to build objects that are immutable. Here’s an example:

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

As you can see from the above examples, the class naming strategy is still the default, and the root cause is not following the above principles. Therefore, the coding process should be based on this principle.

Load class

The dynamicType. Unloaded in the last section represents a class that has not been loaded. As the name implies, these types are not loaded into the Java virtual machine, it just signifies that the bytecode of the class is created. You can get the bytecode through the getBytes method in dynamicType. Unloaded. In your application, you may need to save the bytecode to a File, or to an Unloaded JAR File, so this type also provides a saveIn(File) method, You can store classes in a given folder; The Inject (File) method injects the class into an existing Jar File, and you only need to load the bytecode directly into the virtual machine, which you can do with ClassLoadingStrategy.

If you do not specify ClassLoadingStrategy, Byte Buffer is derived according to offer you this one strategy, the built-in strategy defined in the enumeration ClassLoadingStrategy. The Default

  • WRAPPER: Create a new Wrapping class loader
  • CHILD_FIRST: Similar to above, but the child loader is responsible for loading the target class first
  • INJECTION: Uses reflection to inject dynamic types

The sample

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

So we create and load a class. We use the WRAPPER policy to load classes that are appropriate for most cases. The getLoaded method returns an instance of a Java Class that represents the dynamic Class being loaded.

Reload classes

Thanks to the HostSwap feature of the JVM, loaded classes can be redefined:

Bytebuddyagent.install (); // Install the ByteBuddyAgent. 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"));Copy the code

As you can see, even existing objects are affected by class Reloading. But it should be noted that HostSwap has limitations:

  • Classes must have the same Schema before and after reloading, i.e., methods and fields cannot be reduced (can be added).
  • Classes with static initializer blocks are not supported

Modify the class

redefine

When redefining a class, Byte Buddy can add attributes and methods to an existing class or remove an existing method implementation. If the signature of a newly added method is the same as that of the original method, the original method disappears.

rebase

It’s similar to Re-define, but the original method doesn’t go away. Instead, it’s renamed, with the suffix $original, so that no implementation is lost. Redefined methods can continue to call the original method with their renamed name, such as a class:

class Foo { String bar() { return "bar"; }}Copy the code

Rebase after:

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

Methods intercept

Intercept by matching pattern

ByteBuddy provides a number of DSLS for matching methods, as shown below:

Foo dynamicFoo = new ByteBuddy().subclass(foo.class) // Matches the method declared by 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();Copy the code

ByteBuddythroughnet.bytebuddy.matcher.ElementMatcherTo define configuration policies. You can use this interface to implement your own matching policies. The library itself provides a lot of Matcher.

Methods the entrusted

Using MethodDelegation, you can delegate method calls to any POJO. Byte Buddy does not require the same Source and Target method names

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");Copy the code

Target can also be implemented 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(); }
}Copy the code

The former implementation is easy to understand because there is only one method and the type matches, but what about the latter, which method does Byte Buddy delegate to? Byte Buddy follows one of the closest principles:

  • Intercept (int) passes because argument types do not match
  • The other two methods have matching arguments, but the Intercept (String) type is closer and therefore delegated to it

It is also important to note that the intercepted method must be declared public, otherwise interception enhancement cannot be implemented. In addition, you can annotate methods with the @runtimeType annotation

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

Parameter binding

We can use annotation injection parameters in the Target interceptor method Intercept. ByteBuddy will inject the appropriate parameter values based on the annotation. Such as:

Void intercept(Object o1, Object O2) // Equivalent to void intercept(@argument (0) Object o1, @argument (1) Object O2)Copy the code

Common annotations are as follows:

annotations describe
@Argument Bind a single parameter
@AllArguments Bind an array of all parameters
@This The dynamically generated object that is currently intercepted
@DefaultCall Call the default method instead of the super method
@SuperCall Method used to invoke the parent version
@RuntimeType Can be used on return values, parameters, and prompts ByteBuddy to disable strict type checking
@Super The parent of the currently intercepted, dynamically generated object
@FieldValue Inject the value of one of the fields of the intercepted object

The field properties

public class UserType { public String doSomething() { return null; } } public interface Interceptor { String doSomethingElse(); } public interface InterceptionAccessor { Interceptor getInterceptor(); void setInterceptor(Interceptor interceptor); } public interface InstanceCreator { Object makeInstance(); } public class HelloWorldInterceptor implements Interceptor { @Override public String doSomethingElse() { return "Hello World!" ; } } Class<? extends UserType> dynamicUserType = new ByteBuddy() .subclass(UserType.class) .method(not(isDeclaredBy(Object.class))) Intercept (methoddelegation.tofield ("interceptor")) delegates to the property field interceptor .defineField("interceptor", Interceptor.class, Visibility. PRIVATE) / / define a property field. Implement (InterceptionAccessor. Class). Intercept (FieldAccessor. OfBeanProperty ()) / / implementation Interceptionaccessor.make ().load(getClass().getClassLoader()).getLoaded(); InstanceCreator factory = new ByteBuddy() .subclass(InstanceCreator.class) .method(not(isDeclaredBy(Object.class))) // The superclass Object methods declared. Intercept (MethodDelegation. ToConstructor (dynamicUserType)) / / entrusted to intercept method to invoke the type constructor. Make () .load(dynamicUserType.getClassLoader()) .getLoaded().newInstance(); UserType userType = (UserType) factory.makeInstance(); ((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor()); String s = userType.doSomething(); System.out.println(s); // Hello World!Copy the code

The above example implements the UserType class as an InterceptionAccessor interface, and uses methodDelegation.tofield to delegate the intercepted methods to the new fields.

End

This article is a basic tutorial that I have put together after learning ByteBuddy. Thank you for reading!!

Wechat official account: ByteZ, for more learning materials