×
Community Blog AOP on the "JVM": Practical Implementation of Java Agent

AOP on the "JVM": Practical Implementation of Java Agent

This article delves into the implementation of AOP on the Java platform, focusing on the Spring AOP framework and its limitations when applied in real-world projects.

By Yiqi

This article delves into the implementation of Aspect Oriented Programming (AOP) on the Java platform, focusing on the Spring AOP framework and its limitations when applied in real-world projects. Using Diagnose, a logging framework widely adopted within the team, as an example, the article highlights the constraints of Spring AOP when dealing with non-Bean class methods, static methods, and internal calls.

1. Overview of AOP: Using Diagnose as an Example

When it comes to the implementation of AOP, many might immediately think of Spring AOP. Spring AOP encapsulates the logic related to Cglib and JDK dynamic proxies, providing a convenient way to generate dynamic proxy objects, thereby easily achieving the aspect logic before and after method executions. Many common logging frameworks, permission verification frameworks (Apache Shiro), and RPC invocation frameworks (Apache Dubbo) implement their aspect logic through integration with Spring AOP.

Our team also has a widely used logging framework called Diagnose, whose aspect logic is implemented via Spring AOP. In short, the effect achieved through AOP is that for methods annotated with @Diagnosed, once they are executed, all input parameters, return values, and log information during execution are recorded. These details are then linked together to form a call stack displayed on the frontend, facilitating troubleshooting and tracing.

As shown in the figure below, when a method of a Bean object is annotated with @Diagnosed, once the method is executed, related call information will be printed out on the frontend.

1
2

Eventually, as more and more methods are annotated with @Diagnosed, the call information of a business process is linked together.

Of course, Diagnose distinguishes each trace through users and diagnostic scenarios.

2. Three Major Limitations of Spring AOP

The Diagnose feature can meet the needs of most scenarios. However, when implemented with Spring AOP, Diagnose still has unavoidable limitations:

1) The method annotated with @Diagnosed must be a method of a Bean object. This is easy to understand because the aspect logic operations are performed via the BeanPostProcessor during the creation of a Bean. If it is not a Bean, it cannot be delegated to the BeanPostProcessor, and thus, aspect functionality cannot be applied. This means the call information of non-Bean class methods cannot be recorded by Diagnose.

2) The method annotated with @Diagnosed cannot be a static one. This is because Cglib and JDK dynamic proxy - the two implementation methods of Spring AOP - create dynamic proxies by generating subclasses of the target class and implementing the target interface, respectively. Since static methods cannot be rewritten by a subclass, let alone implemented through an interface.

3) The method annotated with @Diagnosed must be called from outside for the aspect logic to take effect; internal calls such as this.xxx() cannot trigger AOP. This is the scenario that will be focused on in this article.

The first two limitations are easy to understand. Below, we will focus our analysis on the third limitation.

First, let us clarify what "being called from outside" means. Assume there is Bean A, which has three methods, namely the public methods foo and bar and the private method wof. Among these, the foo method calls both bar and wof internally within class A.

@Component
public class A {
    @Diagnosed(name = "foo")
    public void foo() {
        bar();
        wof();
    }

    @Diagnosed(name = "wof")
    private void wof() {
        System.out.println("A.wof");
    }

    @Diagnosed(name = "bar")
    public void bar() {
        System.out.println("A.bar");
    }
}

Then, assume there is Bean B, which injects Bean A and calls the foo method from outside of class A, as shown below:

@Component
public class B {
    @Resource
    private A a;

    public void invokeA() {
        a.foo();
    }
}

In this scenario, the methods foo, wof, and bar within A would all be called, and they are all annotated with @Diagnose. Which method’s diagnostic logs will be printed? In other words, which method’s AOP aspect logic will take effect?

The answer is that only the aspect logic of foo will take effect, while that of wof and bar will not.

Through decompilation, in the generated class of A's dynamic proxy, the wof method does not have any aspect logic; whereas the bar method has aspect logic, but it does not take effect. Thus, two questions arise:

  1. Why is the AOP-related aspect logic not woven into the wof method in the decompiled class?
  2. Why does the AOP-related aspect logic present in the bar method not take effect?

First, let us analyze the first question, which is an issue inherent to all runtime AOP solutions. Whether using the Cglib’s or the JDK’s built-in dynamic proxy, the essence is to define a new Class at runtime. The new Class must be either an interface implementation class or a subclass of the original Class, because without being an implementation of an interface or a subclass, it cannot be injected into the code references.

Take HSF, which we use the most, as an example. In the code, we would reference an HSF remote service in the following way.

@Resource
MyHsfRemoteService myHsfRemoteService;

HSF generates a dynamic proxy class for the MyHsfRemoteService interface and defines a new Class object at runtime that also implements the MyHsfRemoteService interface, except that the calls to the interface methods are intercepted and changed to remote calls. This process strictly confines the new Class object defined by the dynamic proxy to be an implementation class of MyHsfRemoteService; otherwise, it cannot be injected into the bean reference myHsfRemoteService. Cglib, which implements dynamic proxies through inheritance, has the same limitation.

Back to the question, since the wof method is a private method of A, the target Class object generated as a subclass of A cannot perceive the existence of the private method wof of the parent class, thus it does not weave the related aspect logic into wof.

After explaining wof, let us look at bar. As a public method, the bar method in the generated Class is proved through decompilation that it also contains AOP-related aspect logic. So, why does the related aspect logic still not take effect? This question needs to be explained with the generation principle of dynamic proxy classes. In short, the class generated through dynamic proxies executes the woven logic before and after method calls, ultimately forwarding the execution of the method to the source object, which lacks the related aspect logic. As shown in the figure below:

3

Therefore, the third limitation can be further extended, that is, all methods enhanced by AOP must be called from the outside to make the aspect logic effective; calling them internally via the "this" method is ineffective.

3. Java Agent: A Cure for the Issues

The three aforementioned limitations of Spring AOP fundamentally stem from the fact that Spring AOP is a JVM runtime technology. At this point, the class files have already been loaded, and Spring AOP cannot modify the source class files. It can only redefine a class through subclass inheritance or interface implementation, and then replace the original bean with this newly generated class.

Java Agent can perfectly avoid this defect. It is not a new technology and has been available since JDK 1.5. In brief, Java Agent provides developers with a JVM-level extension point, allowing the direct modification of class bytecode when the JVM starts. Using Java Agent does not require generating a new Class; instead, it modifies the existing Class at startup, thus avoiding the constraints of inheritance/interface implementation and the limitations on static method and internal method calls.

The steps to use Java Agent are as follows:

1) Define an object, which contains a static method named premain with parameters String agentArgs and Instrumentation instrumentation.

2) In the resources folder, define the META-INF/MANIFEST.MF file, where you specify the Premain-Class: to point to the object just defined.

3) Package the above MANIFEST.MF file and the premain object into a JAR file, and specify this JAR file with the -javaagent parameter when starting the JVM.

In this way, the JVM will execute the premain method in the JAR file at startup. You can modify the bytecode files of specific classes and methods in the premain method to achieve "AOP" when the JVM starts. In practice, Java Agent is often combined with Bytebuddy (a library for creating and modifying Java classes, commonly used in bytecode manipulation scenarios) to more conveniently achieve bytecode modification.

Below is my practical implementation with Java Agent + Bytebuddy to enhance Diagnose, aiming to make the @Diagnose annotation effective for internal "this" calls and external static method calls.

3.1 Premain

The agentArgs parameter of Premain can be used to pass parameters at startup. We can leverage this feature to pass in some file name prefixes, with the aim of performing subsequent transform operations only on the classes we care about.

4

Once matching is done, use .transform to specify a Transformer. Here, I define a DiagnoseTransformer to handle the modification of the Class bytecode.

3.2 DiagnoseTransformer

DiagnoseTransformer needs to filter methods again, matching those annotated with @Diagnosed, and then use .intercept to delegate the method execution. Here, I define a SelfInvokeMethodInterceptor and delegate the method execution to it.

5

Inside SelfInvokeMethodInterceptor, the actual AOP logic can be executed, which involves the operations related to each AOP business. For Diagnose, I fetch the DiagnosedMethodInterceptor Bean object from the ApplicationContext. This Bean is a method interceptor defined by the Diagnose framework itself, containing the logic for parsing and saving specific method execution information, which will not be shown here.

6

The final package structure is as follows:

7

3.3 Packaging Process

During packaging, note that the premain method runs in the JAR file that is packaged, not in the business JAR file. Therefore, the packaged JAR file must include related dependencies. Here, the "jar-with-dependencies" method is used to include related dependencies in the JAR file.

8

3.4 Specify JVM Parameters

In the APP-META/docker-config/environment/common/bin/setenv.sh file of the application that needs to use Java Agent, add the following line:

SERVICE_OPTS="${SERVICE_OPTS} -javaagent:/home/admin/${APP_NAME}/target/${APP_NAME}/BOOT-INF/lib/diagnose-agent-1.3.0-SNAPSHOT-jar-with-dependencies.jar=com.taobao.gearfactory,com.taobao.message"

Here, com.taobao.gearfactory,com.taobao.message specifies the package paths that need to transform bytecode, which each application needs to define on its own.

3.5 Analysis of Class Loading Pitfalls

After performing the preceding operations, you may encounter an error during application startup: ClassNotFoundException.

This happens because the premain method is executed in the AppClassLoader, and the Java Agent JAR file is also loaded into the AppClassLoader, while our applications are Spring Boot applications, and Spring Boot defines a subclass loader of AppClassLoader called LaunchedURLClassLoader to achieve the effect of having all dependencies contained in a single JAR file. In other words, our business code actually runs in the LaunchedURLClassLoader.

Once we introduce business-related dependencies into the AppClassLoader, it leads to classes that should be loaded by the LaunchedURLClassLoader being delegated to the AppClassLoader. For example, in the DiagnoseTransformer class within the Java Agent JAR, defined classes such as Diagnose, log4j, and ApplicationContext are loaded by the AppClassLoader. However, since our AppClassLoader only has class definitions but lacks the necessary dependencies to load these classes (as the related dependencies are in the LaunchedURLClassLoader), a ClassNotFoundException error occurs.

So, how to solve this problem? There are two steps:

1) During bytecode manipulation, for any business-related dependencies, such as the Diagnose class, log4j, and ApplicationContext, define these dependencies and their logic in the business JAR file, that is, loaded by the LaunchedURLClassLoader.

2) In the jar of the agent, obtain this part of the business-related classes through reflection.

As shown in the figure below, separate the SelfInvokeMethodInterceptor class that involves other dependencies from the diagnose-agent package and place it into the diagnose-client package. Ensure that the application depends on this JAR file (the application already depends on the diagnose-client).

The diagnose-agent package only defines dependencies unrelated to the business, such as ByteBuddy. The diagnose-client package defines Spring and log-related dependencies.

9

The business application dependencies are shown in the figure below:

10

In the diagnose-agent, when calling SelfInvokeMethodInterceptor and Diagnose-related classes, use reflection to obtain them. The classLoader parameter of the transform method is the LaunchedURLClassLoader.

11

By doing this, the diagnose-agent will not depend on any other business-related classes, leaving the business-related classes to be loaded by the LaunchedURLClassLoader.

4. Demonstration: Achieve Interception of Private Methods and Static Methods

As shown in the following figure, apply the @Diagnosed annotation to two private methods, decryptBuyerId and getAndCheckOrder.

12
13

Also, apply the @Diagnosed annotation to the static method ResultDTO.fail.

14

Ultimately, the related AOP logic can take effect.

15

5. Conclusion

This article delves into the challenges and solutions for method monitoring through AOP on the Java platform, particularly focusing on the limitations of Spring AOP and its shortcomings in handling internal method calls and static methods. Through a practical example — the use of the logging framework Diagnose — the article reveals the application limitations of Spring AOP in scenarios involving non-Bean methods, static methods, and internal calls, along with a detailed analysis of the technical reasons behind these limitations.

In summary, this article is not only a journey of technical exploration but also a vivid demonstration of how to overcome the limitations of existing technical frameworks, and continuously optimize and innovate, showcasing the limitless possibilities of AOP technology on the Java platform.


Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.

0 1 0
Share on

Alibaba Cloud Community

1,076 posts | 265 followers

You may also like

Comments