Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to create a Java Agent easily by using Byte Buddy

2025-01-22 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/01 Report--

This article mainly shows you "how to create Java Agent easily through the use of Byte Buddy", the content is easy to understand, clear, hope to help you solve your doubts, the following let the editor lead you to study and learn "how to create Java Agent easily through the use of Byte Buddy" this article.

Java agent is a Java program to be executed before another Java application (the "target" application) is launched, so that agent has the opportunity to modify the target application or the environment in which the application is running. In this article, we will start with the basic content and gradually enhance its functionality, making it an advanced agent implementation with the help of the bytecode manipulation tool Byte Buddy.

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

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

As mentioned in the code above, the definition of Java agent is similar to that of other Java programs, except that it uses the premain method instead of the main method as the entry point. As the name implies, this method can be executed before the main method of the target application. Compared to other Java programs, there are no specific rules for writing agent. One small difference is that Java agent accepts an optional parameter rather than an array of zero or more parameters.

If you want to use this agent, you must package the agent class and resources into jar, and in the manifest of jar, set the Agent-Class property to the agent class that contains the premain method. (agent must be packaged in a jar file, which cannot be specified by the format of the disassembly. Next, we need to start the application and refer to the location of the jar file through the javaagent parameter on the command line:

Java-javaagent:myAgent.jar-jar myProgram.jar we can also set optional agent parameters on the location path. In the following command, a Java program is started and the given agent is added, providing the value myOptions as an argument to the premain method:

Java-javaagent:myAgent.jar=myOptions-jar myProgram.jar can add multiple agent by repeatedly using the javaagent command.

However, the function of Java agent is not limited to modifying the state of the application environment, Java agent can access Java instrumentation API, so that agent can modify the code of the target application. This little-known feature in the Java virtual machine provides a powerful tool for aspect-oriented programming.

To make this change to the Java program, we need to add a second parameter of type Instrumentation to the premain method of agent. The Instrumentation parameter can be used to perform a range of tasks, such as determining the exact size of the object in bytes and actually modifying the implementation of the class by registering ClassFileTransformers. After ClassFileTransformers is registered, it is called whenever the class loader (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 it represents is loaded. In this way, we can enhance or modify the behavior of the class before it is used, 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 has not been loaded before, the value is null ProtectionDomain protectionDomain, byte [] class) {/ / returns the changed class file. });}}

After registering the above ClassFileTransformer with an Instrumentation instance, this transformer is called when each class is loaded. To achieve this, transformer accepts a reference to a binary and a class loader, representing the class file and the class loader that is trying to load the class, respectively.

Java agent can also be registered at the run time of the Java application, and in this scenario, instrumentation API allows you to redefine loaded classes, a feature called "HotSwap". However, redefining the class is limited to the replacement method body. When a class is redefined, class members cannot be added or removed, and types and signatures cannot be modified. There is no such restriction when the class is first loaded, and if it is in such a scenario, classBeingRedefined will be set to null.

Java bytecode and class file format

The class file represents the compiled state of the Java class. The class file contains bytecode, which represents the original program instructions in the Java source code. Java bytecode can be regarded as the language of the Java virtual machine. In fact, JVM does not treat Java as a programming language, it can only deal with bytecode. Because it is expressed in binary form, it takes up less space than the source code of the program. In addition, representing programs in bytecode makes it easier to compile languages other than Java, such as Scala or Clojure, so that they run on JVM. If there is no bytecode as an intermediate language, other programs may need to convert it to Java source code before running it.

However, this abstraction comes at a cost when the code is processed. If you want to apply ClassFileTransformer to a class, you can't process that class as Java source code, or even assume that the converted code was originally written by Java. To make matters worse, reflective API for probing class members or annotations is also prohibited because we cannot access these API until the class is loaded and cannot be loaded until the conversion process is complete.

Fortunately, Java bytecode is a relatively simple abstract form that contains a small number of operations that can be roughly mastered with a little effort. When the Java virtual machine executes the program, it processes the values in a stack-based manner. Bytecode instructions generally tell the virtual machine that you need to eject the value from the Operand stack (operand stack), perform some operations, and then press the result onto the stack.

Let's consider a simple example: add the numbers 1 and 2. JVM first pushes these two numbers onto the stack, which is done through two byte instructions, _ iconst_1_ and _ iconst_2_. _ iconst_1_ is a single-byte convenience operator (operator) that pushes the number 1 onto the stack. Similarly, _ iconst_2_ pushes the number 2 onto the stack. The _ iadd_ instruction is then executed, which pops up the latest two values in the stack and presses the result of their summation back on the stack. In a class file, each instruction is not stored in its memorable name, but in the form of a byte that uniquely marks a specific instruction, which is where the term _ bytecode_ comes from. The bytecode instructions described above and their impact on the Operand stack are visualized through the picture below.

Human users will prefer source code to bytecode, but fortunately the Java community has created multiple libraries that can parse class files and expose compact bytecode as a named instruction stream. For example, the popular ASM library provides a simple visitor API that parses class files into member and method instructions in a manner similar to the SAX parser when reading XML files. If ASM is used, the bytecode in the above example can be implemented as follows (in this case, the instruction in ASM mode is visitIns, which can provide a modified method implementation):

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

It is important to note that the bytecode specification is nothing more than a metaphorical metaphor, because the Java virtual machine allows programs to be converted into optimized machine code (machine code), as long as the output of the program is guaranteed to be correct. Because of the simplicity of bytecode, it is simple and straightforward to replace and modify instructions in existing classes. Therefore, using ASM and its underlying Java bytecode base is sufficient for class-transformed Java agent, which requires registering a ClassFileTransformer that uses this library to process its parameters.

Overcome the deficiency of bytecode

For practical applications, parsing the original class file still means a lot of manual work. Java programmers are usually interested in classes in a type hierarchy. For example, a Java agent might need to modify all classes that implement a given interface. If you want to determine the superclass of a class, it is not enough to parse the class file given by ClassFileTransformer, which contains only the names of the direct superclass and interface. In order to analyze possible supertype associations, programmers still need to locate these types of class files.

Another difficulty in using ASM directly in a project is that developers need the basics of learning Java bytecode on the team. In practice, this often causes many developers to be afraid to modify the code related to bytecode operations. If so, implementing Java agent can easily pose risks to the long-term maintenance of the project.

To overcome these problems, it is best to implement Java agent at a higher level of abstraction rather than directly manipulating Java bytecode. Byte Buddy is an open source library based on the Apache 2.0 license that addresses the complexity of bytecode manipulation and instrumentation API. The stated goal of Byte Buddy is to hide explicit bytecode operations behind a type-safe domain-specific language. By using Byte Buddy, bytecode manipulation is expected to be very easy for anyone familiar with the Java programming language.

Introduction to Byte Buddy

The purpose of Byte Buddy is not just to generate Java agent. It provides an API for generating arbitrary Java classes, and the API,Byte Buddy based on this generated class provides additional API to generate Java agent.

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

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 above code, we can see that there are two steps for Byte Buddy to implement a method. First, the programmer needs to specify an ElementMatcher, which is responsible for identifying one or more methods that need to be implemented. Byte Buddy provides feature-rich predefined interceptors (interceptor) that are exposed in the ElementMatchers class. In the above example, the toString method matches the name exactly, but we can also match more complex code structures, such as types or annotations.

When Byte Buddy generates a class, it analyzes the class hierarchy of the generated type. In the above example, Byte Buddy was able to determine that the generated class would inherit a method named toString of its superclass Object, and the specified matcher would require Byte Buddy to override the method through the subsequent

Implementation

Instance, in our example, that is,

FixedValue

When creating a subclass, Byte Buddy always intercepts (intercept) a matching method and overrides it in the generated class. However, we will see later in this article that Byte Buddy can also redefine existing classes without having to implement them as subclasses. In this case, Byte Buddy replaces the existing code with the generated code and copies the original code into another synthetic method.

In our code example above, the matching method is rewritten, and in the implementation, the fixed value "Hello World!" is returned. The intercept method accepts parameters of type Implementation, and Byte Buddy comes with several predefined implementations, such as the FixedValue class used above. However, if necessary, you can use the ASM API described earlier to implement a method as custom bytecode, and Byte Buddy itself is based on ASM API.

Once the properties of the class are defined, it can be generated through the make method. In the sample application, because the user does not specify a class name, the generated class is given an arbitrary name. Eventually, the generated class will be loaded using ClassLoadingStrategy. By using the default WRAPPER policy above, the class will be loaded using a new classloader that uses the environment classloader as the parent loader.

After the class is loaded, you can access it using the Java reflection API. If no other constructor is specified, Byte Buddy will generate a constructor similar to the parent class, so the generated class can use the default constructor. This way, we can verify that the generated class overrides the toString method, 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 run time, which depends on the parameters of the method and the state of the object.

Implement Instrumentation through delegation

A more flexible way to implement a method is to use Byte Buddy's MethodDelegation. By using a method delegate, it is possible to call other methods of a given class and instance when generating an overridden implementation. In this way, we can rewrite the above example using the following delegator:

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

Using the delegator above, Byte Buddy determines the _ optimal calling method _ in the intercept target given by the to method. In the case of ToStringInterceptor.class, the selection process is simply the only static way to parse this type. In this case, only one static method is considered because a _ class _ is specified in the delegate's target. In contrast, we can also delegate it to _ instance _ of a class, and if so, Byte Buddy will consider all virtual virtual method. If there are multiple such methods on a class or instance, Byte Buddy first excludes all methods that are incompatible with the specified instrumentation. In the remaining methods, the library will choose the best match, which is usually the method with the most parameters. We can also explicitly specify the target method, which requires narrowing the scope of the legal method, passing the ElementMatcher to the MethodDelegation, and the method will be filtered. For example, only a method named "intercept" is considered a delegate target by adding the following filter,Byte Buddy:

MethodDelegation.to (ToStringInterceptor.class) .filter (ElementMatchers.named ("intercept"))

After performing the above interception, the intercepted method still prints "Hello World!", but this time the result is calculated dynamically, so that we can set a breakpoint on the interceptor method, and the generated class triggers the interceptor method every time it calls toString.

When we set parameters for the interceptor method, we can unleash the full power of MethodDelegation. The parameters here are usually annotated and are used to require Byte Buddy to inject a specific value when calling the interceptor method. For example, by using the @ Origin annotation, Byte Buddy provides an example of a method to add instrument functionality as an instance of a class in the Java reflection API:

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

When intercepting the toString method, the call to the instrument method will return "Hello world from toString!".

In addition to the @ Origin annotation, Byte Buddy provides a set of feature-rich annotations. For example, by using the @ Super annotation on a parameter of type Callable, Byte Buddy creates and injects a proxy instance that can call the original code of the instrument method. If the annotations provided do not meet the requirements or are not suitable for a particular user scenario, we can even register custom annotations to inject them into user-specific values.

Implement method-level security

As you can see, we can use MethodDelegation to dynamically rewrite a method at run time with the help of simple Java code. This is just a simple example, but this technique can be used in more practical applications. For the rest of this article, we will develop a sample that uses code generation techniques to implement an annotation-driven library to limit method-level security. In our first iteration, the library restricts security by generating subclasses. Then, we will take the same way to implement Java agent and complete the same function.

The sample library uses the following annotations to allow the user to specify that a method needs to consider security factors:

@ interface Secured {String user ();}

For example, suppose the application needs to use the following Service class to perform sensitive operations, and only the user is authenticated as an administrator to execute the method. This is specified by declaring Secured annotations for the method that performs 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 practice, hard-coding crosscutting concerns often leads to copy-and-paste logic, making it difficult to maintain. In addition, once the application needs to involve additional requirements, such as logging, collecting call metrics, or result caching, it is not good to add such code scalability directly. By extracting such functionality into agent, the method can focus purely on its business logic, making the code base easier to read, test, and maintain.

In order to keep our planned library as simple as possible, according to the annotated protocol statement, an IllegalStateException exception will be thrown if the current user does not have the user attributes of the annotated. By using Byte Buddy, this behavior can be achieved with a simple interceptor, as shown in SecurityInterceptor in the following example, which tracks the login 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");}

From the above code, we can see that even if a given user is granted access, the interceptor does not call the original method. To solve this problem, Byte Buddy has many predefined ways to implement functional links. With the andThen method of the MethodDelegation class, the above security check can be placed before the call to the original method, as shown in the following code. If the user does not authenticate, the security check will throw an exception and prevent subsequent execution, so the original method will not be executed.

By putting these functions together, we can generate a subclass of Service, and all annotated methods can be properly secured. Because the generated class is a subclass of Service, it can replace all variables of type Service without any type conversion. If not properly authenticated, calling the doSensitiveAction method 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 ()

The bad news, though, is that because subclasses that implement instrumentation functionality are created at run time, there is no way to create such an instance other than using Java reflection. Therefore, all instances of instrumentation classes should be created through a factory that encapsulates the complexity of creating instrumentation subclasses. As a result, the subclass instrumentation is often used in frameworks, which themselves need to create instances through factories, such as dependency management framework Spring or object-relational mapping framework Hibernate, while subclass instrumentation is often too complex to implement for other types of applications.

Java agent that implements security function

By using Java agent, an alternative implementation of the above security framework will modify the original bytecode of the Service class instead of rewriting it. In doing so, there is no need to create a managed instance, just simply call the

New Service (). DoSensitiveAction () is fine, and an exception is thrown if the corresponding user is not authenticated. To support this approach, Byte Buddy provides an idea called _ rebase some class _. When you rebase a class, no subclasses are created, but the strategy is that the code that implements the instrumentation function will be merged into the class being instrument, thus changing its behavior. After adding the instrumentation function, the original code of all its methods can be accessed in the instrument class, so instrumentation like SuperMethodCall works exactly the same way as creating subclasses.

The behavior of creating a subclass is very similar to that of rebase, so the API execution of the two operations is the same, using the same DynamicType.Builder interface to describe a type. Both forms of instrumentation can be accessed through the ByteBuddy class. To facilitate the definition of Java agent, Byte Buddy also provides the AgentBuilder class, which wants to be able to deal with some common user scenarios in a concise way. To define security at the Java agent implementation method level, defining the following class as the entry point for agent is sufficient to accomplish this:

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 the agent is packaged as a jar file and specified on the command line, all methods annotated with Secured will be "converted" or redefined for security. If the Java agent is not activated, the application does not include additional security checks at run time. Of course, this means that if annotated code is unit tested, calls to these methods do not require a special build process to simulate the security context. The Java runtime ignores annotation types that cannot be found in classpath, so when running annotated methods, we can even remove the security library from the application.

Another advantage is that Java agent can be easily overlaid. If you specify more than one Java agent on the command line, each agent has the opportunity to modify the class in the order specified on the command line. For example, we can combine security, logging, and monitoring frameworks in this way without adding any form of integration layer between these applications. Therefore, the concern of using Java agent to implement crosscutting provides a more modular way of writing code without having to integrate all the code against the central framework of a management instance.

The source code for _ Byte Buddy is available free of charge on GitHub. The getting started manual can be found on http://bytebuddy.net. The currently available version of Byte Buddy is 0.7.4, and all samples are based on that version. The library won the Oracle Duke's Choice award in 2015 for its innovative nature and contribution to the Java ecosystem.

The above is all the content of the article "how to easily create Java Agent by using Byte Buddy". Thank you for reading! I believe we all have a certain understanding, hope to share the content to help you, if you want to learn more knowledge, welcome to follow the industry information channel!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report