By using Byte Buddy, you can easily create Java Agent and bytebuddy

Source: Internet
Author: User
Tags getloaded

By using Byte Buddy, you can easily create Java Agent and bytebuddy

The Java agent is the Java program to be executed before another Java application ("target" Application) is started. In this way, the agent has the opportunity to modify the environment where the target application or application runs. In this article, we will gradually enhance the basic content and use Byte Buddy as an advanced agent.

In the most basic cases, Java agent is used to set application properties or configure specific environment states. The agent can be used as a reusable and pluggable component. The following example describes an agent that sets a system attribute and can be used in a program:

public class Agent {  public static void premain(String arg) {    System.setProperty("my-property", “foo”);  }}

As described in the Code above, the definition of Java agent is similar to that of other Java programs, except that it usespremainThe method replaces the main method as the entry point. As the name suggests, this method can be executed before the main method of the target application. Compared with other Java programs, there are no specific rules for compiling the agent. A small difference is that Java agent accepts an optional parameter, rather than an array containing zero or more parameters.

To use this agent, you must package the agent class and resources in jar, andAgent-ClassSet property to includepremainThe agent class of the method. (The agent must be packaged into a jar file. It cannot be specified by the disassembling format .) Next, we need to start the application and reference the location of the jar file through the javaagent parameter in the command line:

java -javaagent:myAgent.jar -jar myProgram.jar

You can also set the optional agent parameters in the location path. In the following command, a Java program is started and a given agent is added. The value of myOptions is providedpremainMethod:

java -javaagent:myAgent.jar=myOptions -jar myProgram.jar

ReusejavaagentCommand to add multiple agents.

However, the Java agent function is not limited to modifying the state of the application environment. The Java agent can access the Java instrumentation API, so that the agent can modify the code of the target application. This little-known feature in Java virtual machines provides a powerful tool to help implement cross-section programming.

To modify the Java programpremainMethodInstrumentation. The Instrumentation parameter can be used to execute a series of tasks, such as determining the exact size of an object in bytes andClassFileTransformersThe implementation of the actual modification class.ClassFileTransformersAfter registration, it is called when the class loader loads the class. When it is called, the class file transformer has the opportunity to change or completely replace the class file before the class file represents the class file. In this way, before using the class, we can enhance or modify the class behavior, as shown in the following example:

Public class Agent {public static void premain (String argument, Instrumentation inst) {inst. addTransformer (new ClassFileTransformer () {@ Override public byte [] transform (ClassLoader loader, String className, Class <?> ClassBeingRedefined, // if the class is not loaded before, the value is null ProtectionDomain protectionDomain, byte [] classFileBuffer) {// return the changed class file. }});}}

UseInstrumentationInstance RegistrationClassFileTransformerThe transformer will be called when each class is loaded. To achieve this, transformer accepts a binary and Class Loader reference, representing the class file and the class loader trying to load the class respectively.

Java agent can also be registered at the runtime of Java applications. In this scenario, the instrumentation API allows you to redefine loaded classes. This feature is called "HotSwap ". However, the redefinition class is limited to replacing the method body. When you redefine a class, you cannot add or remove class members, and the type and signature cannot be modified. This restriction is not applied when the class is loaded for the first time. If this is the caseclassBeingRedefinedIs set to null.

Java bytecode and class file format

The class file represents the status after the Java class is compiled. Class files contain bytecode, which represent the original program instructions in the Java source code. Java bytecode can be considered as the language of the Java Virtual Machine. In fact, JVM does not regard Java as a programming language. It can only process bytecode. Because it uses binary representation, it occupies less space than the source code of the program. In addition, it is easier to compile programs in the form of bytecode to compile languages other than Java, such as Scala or Clojure, so that they can run on the JVM. If no bytecode is used as the intermediate language, other programs may need to convert it to the Java source code before running.

However, this abstraction brings about a certain cost during code processing. If you wantClassFileTransformerWhen a class is applied, we cannot process the class in the form of Java source code, or even assume that the converted code was originally written in Java. What's worse, the reflection APIs of probe class members or annotations are also forbidden, because before the class is loaded, we cannot access these Apis. Before the conversion process is complete, cannot be loaded.

Fortunately, Java bytecode is a relatively simple abstract form, which contains a small number of operations. We can master it with a little effort. When the Java virtual machine executes a program, it processes the value in stack-based mode. The bytecode command usually tells the virtual machine that a value needs to pop up from the operand stack to execute some operations and then press the result into the stack.

Let's consider a simple example: adding numbers 1 and 2. JVM will first press these two numbers into the stack, which is achieved through the iconst_1 and iconst_2 byte commands. Iconst_1 is a single-byte convenient operator (operator) that presses number 1 into the stack. Similarly, iconst_2 presses the number 2 to the stack. Then, the iadd command will be executed, and the latest two values in the stack will pop up, and the results of their sum calculation will be re-pressed to the stack. In a class file, each instruction is stored in a byte instead of a name that is easy to remember. This byte can uniquely mark a specific instruction, this is also the term bytecode. The bytecode commands described above and their impact on the operand Stack are visualized through the following picture.

 

For human users, they prefer source code rather than bytecode. Fortunately, the Java Community has created multiple libraries, ability to parse class files and expose compact bytecode as command streams with names. For example, the popular ASM Library provides a simple visitor API that analyzes class files as members and method commands. The operation is similar to the SAX Parser when reading XML files. If ASM is used, the bytecode in the above example can be implemented according to the following code (here, the command in ASM mode isvisitIns):

MethodVisitor methodVisitor = ...methodVisitor.visitIns(Opcodes.ICONST_1);methodVisitor.visitIns(Opcodes.ICONST_2);methodVisitor.visitIns(Opcodes.IADD);

It should be noted that the bytecode specification is just a metaphor (metaphor), because the Java Virtual machine allows the program to be converted to the optimized machine code ), as long as the program output can be ensured to be correct. Because of the simplicity of bytecode, replacing and modifying instructions in existing classes is simple and straightforward. Therefore, the use of ASM and its underlying Java bytecode is sufficient for Java agents that implement class conversion.ClassFileTransformerIt uses this library to process its parameters.

Overcome bytecode Deficiency

For practical applications, parsing the original class file still means a lot of manual work. Java programmers are usually interested in classes in the type hierarchy. For example, a Java agent may need to modify all classes that implement the given interface. If you want to determine the superclass of a class, it only depends on ParsingClassFileTransformerThe given class file is not enough. The class file only contains the name of the direct superclass and interface. To resolve possible super-type associations, programmers still need to locate these types of class files.

Another difficulty in using ASM directly in the project is that the team needs developers to learn the basics of Java bytecode. In practice, this often causes many developers to not modify the bytecode operation-related code. In this case, the implementation of Java agent can easily bring risks to the long-term maintenance of the project.

To overcome these problems, we 'd better use high-level abstraction to implement Java agent, rather than directly operating Java bytecode. Byte Buddy is an open-source Library Based on the Apache 2.0 license. It is designed to solve the complexity of Bytecode operations and instrumentation API. The goal of Byte Buddy is to hide explicit Bytecode operations behind a type-safe domain-specific language. By using Byte Buddy, anyone familiar with the Java programming language is expected to perform Bytecode operations very easily.

About Byte Buddy

The purpose of Byte Buddy is not only to generate a Java agent. It provides an API to generate any Java class. Based on this API, Byte Buddy provides additional APIs to generate the Java agent.

As an introduction to Byte Buddy, the following example shows how to generate a simple class. This class is a subclass of the Object and overwrites the toString method to return "Hello World !". Similar to the original ASM, "intercept" will tell Byte Buddy to provide method implementation for the intercepted command:

Class<?> dynamicType = new ByteBuddy()  .subclass(Object.class)  .method(ElementMatchers.named("toString"))  .intercept(FixedValue.value("Hello World!"))  .make()  .load(getClass().getClassLoader(),                  ClassLoadingStrategy.Default.WRAPPER)  .getLoaded();

From the code above, we can see that Byte Buddy has two steps to implement a method. First, the programmer needs to specifyElementMatcherIt identifies one or more methods to be implemented. Byte Buddy provides a rich range of predefined interceptors (interceptor), which are exposed inElementMatchersClass. In the preceding example,toStringThe method completely matches the name, but we can also match more complex code structures, such as types or annotations.

When Byte Buddy generates a class, it analyzes the generated class hierarchy. In the above example, Byte Buddy can determine that the generated class should inherit the toString method of its super class Object, and the specified vertex will require Byte Buddy to override this method. Implementation In our example, this instance isFixedValue.

When a subclass is created, Byte Buddy will always intercept(intercept)A matching method, which is rewritten in the generated class. However, we will see later in this article that Byte Buddy can also redefine existing classes without the need to implement them through subclasses. In this case, Byte Buddy replaces the existing code with the generated code, and copies the original code to another synthetic (synthetic) method.

In the code sample above, the matching method is overwritten. in the implementation, a fixed value "Hello World!" is returned !".interceptThe method accepts Implementation parameters. Byte Buddy comes with multiple predefined implementations, as shown in the precedingFixedValueClass. However, if necessary, you can use the asm api described above to implement a method as a custom bytecode. Byte Buddy is also implemented based on the asm api.

After defining the attributes of a class, you can use the make method to generate the class. In the example application, the generated class is given an arbitrary name because the class name is not specified. Eventually, the generated class will useClassLoadingStrategy. By using the above default WRAPPERPolicy, the class will use a new class loader for loading, this class loader will use the environment class loader as the parent loader.

After the class is loaded, you can access it using the Java reflection API. If no constructor is specified, Byte Buddy will generate a constructor similar to the parent class, so the generated class can use the default constructor. In this way, we can check that the generated class is overwritten. toStringMethod, as shown in the following code:

assertThat(dynamicType.newInstance().toString(),            is("Hello World!"));

Of course, this generated class is not very useful. For practical applications, the return values of most methods are calculated at runtime. This calculation process depends on the parameters of the method and the status of the object.

Implement Instrumentation through delegation

To implement a method, there is a more flexible way, that is, using Byte Buddy's MethodDelegation. By using method delegation, we may call other methods of the given class and instance when generating the rewrite implementation. In this way, we can use the following delegate (delegator) to rewrite the above example:

class ToStringInterceptor {  static String intercept() {    return “Hello World!”;  }}

With the above POJO interceptor, we can replace the previous FixedValue implementation with MethodDelegation. to (ToStringInterceptor. class ):

Class<?> dynamicType = new ByteBuddy()  .subclass(Object.class)  .method(ElementMatchers.named("toString"))  .intercept(MethodDelegation.to(ToStringInterceptor.class))  .make()  .load(getClass().getClassLoader(),                  ClassLoadingStrategy.Default.WRAPPER)  .getLoaded();

With the above delegate, Byte Buddy will determine the optimal call method in the interception target specified by the to method. JustToStringInterceptor.classThe selection process is simply the only static method for parsing this type. In this example, only one static method is considered, because the delegate object specifies a class. The difference is that we can also delegate it to a class instance. If so, Byte Buddy will consider all virtual methods ). If there are multiple such methods on the class or instance, Byte Buddy will first exclude all methods that are not compatible with the specified instrumentation. Among the remaining methods, the library will select the best matched method, which is usually the most parameter method. We can also explicitly specify the target method, which needs to narrow down the scope of the valid methodElementMatcherPassMethodDelegationTo filter the methods. For example, add the followingfilter, Byte Buddy only regards the method named "intercept" as the delegate target:

MethodDelegation.to(ToStringInterceptor.class)                .filter(ElementMatchers.named(“intercept”))

After the above interception is executed, the intercepted method still prints "Hello World !", However, the results are calculated dynamically. In this way, we can set a breakpoint on the interceptor method, and the generated class is called each time.toStringWill trigger the interceptor method.

When we set parameters for the interceptor method, we can releaseMethodDelegationAll the power. The parameters here are usually annotated to require Byte Buddy to inject a specific value when calling the interceptor method. For example@OriginNote: Byte Buddy provides an example of adding the instrument function as an example of the class in the Java reflection API:

class ContextualToStringInterceptor {  static String intercept(@Origin Method m) {    return “Hello World from ” + m.getName() + “!”;  }}

When interceptedtoStringIf you call a method, the system returns "Hello world from toString !".

Besides@OriginIn addition to annotations, Byte Buddy provides a rich set of annotations. For exampleCallableUse@SuperNote: Byte Buddy creates and injects a proxy instance, which can call the original code of the instrument method. If the provided annotations cannot meet the requirements or are not suitable for specific user scenarios, we can even register custom annotations to inject these annotations into user-specific values.

Implement method-level security

We can see that a method can be dynamically rewritten using MethodDelegation with simple Java code at runtime. This is just a simple example, but this technology can be used in more practical applications. In the remaining content of this article, we will develop an example that uses the code generation technology to implement an annotation-driven library to limit the security of methods. In our first iteration, this library restricts security by generating sub-classes. Then, we will implement the Java agent in the same way to complete the same functions.

The sample library uses the following annotations to allow users to specify a method that requires consideration of security factors:

@interface Secured {  String user();}

For example, assume that the application needs to use the followingServiceClass to perform sensitive operations, and the method can be executed only when the user is authenticated as the administrator. This is specified by declaring the Secured annotation for the method that executes this operation:

Class Service {@ Secured (user = "ADMIN") void doSensitiveAction () {// run sensitive code ...}}

Of course, we can write the security check directly into the method. In reality, hard-coded cross-cutting concerns often lead to the copy-paste logic, making it difficult to maintain. In addition, once the application requires additional requirements, such as logs, collection call indicators or result caching, directly adding such code will not be very scalable. By extracting such functions into the agent, the method can focus purely on its business logic, making the code library easier to read, test, and maintain.

To keep the planned library as simple as possible, if the current user does not have the user attribute of the annotation according to the annotation protocol declaration, it will throwIllegalStateExceptionException. By using Byte Buddy, this behavior can be implemented with a simple interceptor, as shown in the following example:SecurityInterceptorAs shown in, it tracks the logon of the current user through its static user Domain:

class SecurityInterceptor {  static String user = “ANONYMOUS”  static void intercept(@Origin Method method) {    if (!method.getAnnotation(Secured.class).user().equals(user)) {      throw new IllegalStateException(“Wrong user”);    }  }}

Through the code above, we can see that the interceptor does not call the original method even if the given user has been granted access permissions. To solve this problem, Byte Buddy has many predefined methods to implement functional links. WithMethodDelegationClassandThenMethod, the preceding security check can be placed before the original method is called, as shown in the following code. If the user does not perform authentication, the security check will throw an exception and prevent subsequent execution. Therefore, the original method will not be executed.

By combining these functions, we can generateServiceAll methods with annotations can properly perform security protection. Because the generated class is a subclass of the Service, it can replace all typesServiceDoes not require any type conversion. If not properly authenticated, calldoSensitiveActionMethod will throw an exception:

new ByteBuddy()  .subclass(Service.class)  .method(ElementMatchers.isAnnotatedBy(Secured.class))  .intercept(MethodDelegation.to(SecurityInterceptor.class)                             .andThen(SuperMethodCall.INSTANCE)))  .make()  .load(getClass().getClassLoader(),           ClassLoadingStrategy.Default.WRAPPER)  .getLoaded()  .newInstance()  .doSensitiveAction();

However, the bad message is that the subclass implementing the instrumentation function is created at runtime, so there is no way to create such an instance except using Java reflection. Therefore, all instances of the instrumentation class should be created through a factory, which encapsulates the complexity of creating the instrumentation subclass. As a result, subclass instrumentation is usually used in the framework. These frameworks need to create instances through factories, for example, for applications of other types, the implementation of subclass instrumentation is usually too complex.

Java agent for security

By using Java agent, an alternative implementation of the above security framework will be modifiedServiceClass, rather than rewriting it. In this way, there is no need to create a hosted instance. You just need to call

new Service().doSensitiveAction()

If the user is not authenticated, an exception is thrown. To support this method, Byte Buddy provides a concept called rebase class. When rebase is a class, no subclass will be created. The code used to implement the instrumentation function will be merged into the class of the instrument to change its behavior. After the instrumentation function is added, the original code of all methods of the instrument class can be accessed.SuperMethodCallThis kind of instrumentation works exactly the same way as creating sub-classes.

The behavior of creating subclass and rebase is very similar. Therefore, the API execution methods of the two operations are the same, and the sameDynamicType.BuilderInterface to describe a type. Either of the two forms of instrumentation can be passed throughByteBuddyClass. To facilitate the definition of Java agent, Byte Buddy also provides AgentBuilderClass, which is expected to be able to cope with some common user scenarios in a concise way. To define the Java agent to implement method-level security, defining the following class as the agent entry point is enough to complete this function:

class SecurityAgent {  public static void premain(String arg, Instrumentation inst) {    new AgentBuilder.Default()    .type(ElementMatchers.any())    .transform((builder, type) -> builder    .method(ElementMatchers.isAnnotatedBy(Secured.class)    .intercept(MethodDelegation.to(SecurityInterceptor.class)               .andThen(SuperMethodCall.INSTANCE))))    .installOn(inst);  }}

If you package the agent as a jar file and specify it in the command line, allSecuredAnnotation methods will be "converted" or redefined to achieve security protection. If the Java agent is not activated, the application does not include any additional security check during runtime. Of course, this means that if you perform unit tests on codes with annotations, the calls to these methods do not require a special setup process to simulate the security context. Java runtime ignores annotation types that cannot be found in classpath. Therefore, when running methods with annotations, we can even remove the security library from the application.

Another advantage is that Java agent can be easily superimposed. If multiple Java agents are specified in the command line, each agent has the opportunity to modify the class. The order is the sequence specified in the command line. For example, we can combine security, log, and monitoring frameworks in this way without adding any form of integration layer between these applications. Therefore, the use of Java agent to achieve cross-cutting concerns provides a more modular way of coding, instead of integrating all the code for the central framework of a management instance.

Byte Buddy source code can be obtained on GitHub for free. The Getting Started manual can be found on the http://bytebuddy.net. The current available version of Byte Buddy is 0.7.4, and all samples are based on this version. Because of its innovation and contributions to the Java ecosystem, the database was awarded the Oracle Duke's Choice Award in 2015.

About the author

Rafael Winterhalter is a software consultant working in Oslo, Norway. He is a supporter of static types and has great enthusiasm for JVM, especially focusing on Code instrumentation, concurrency and functional programming. Rafael will write blogs about software development on a daily basis, attend relevant meetings frequently, and is recognized as JavaOne Rock Star. In the coding process outside of work, he has contributed to multiple open-source projects and often spends his energy on Byte Buddy. This is a library generated by code for Java virtual machines to simplify the runtime. Rafael received the Duke's Choice Award for his contribution.

 

Easily Create Java Agents with Byte Buddy

Q: one-click call to programmer Q & A artifacts, one-to-one service, developer programming required official website: www.wenaaa.com

QQ Group 290551701 has gathered many Internet elites, Technical Directors, architects, and project managers! Open-source technology research, welcome to the industry, Daniel and beginners interested in IT industry personnel!

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.