First, background

For details, see -> Intercepting and replacing tokens in SpringBoot to simplify authentication

Ii. Overview of the problem

1. An error message is displayed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]: Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0; nested exception is org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [com.wingli.agent.helper.util.SpringContextHolder] for bean with name 'com.wingli.agent.helper.util.SpringContextHolder': problem with class file or dependent class; nested exception is java.lang.NoClassDefFoundError: org/springframework/context/ApplicationContextAware at ...... Caused by: java.lang.ClassNotFoundException: org.springframework.context.ApplicationContextAware at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~ [? : 1.8.0 comes with _251] at Java lang. This. The loadClass (418). This Java: ~ [? : 1.8.0 comes with _251] the at Sun. Misc. The Launcher $AppClassLoader. LoadClass (355). The Launcher Java: ~ [? : 1.8.0 comes with _251] the at Java. Lang. This. LoadClass (351). This Java: ~ [? : 1.8.0 comes with _251] at Java lang. This. DefineClass1 (Native Method) ~ [? : 1.8.0 comes with _251] at Java lang. This. The defineClass (756). This Java: ~ [? : 1.8.0 comes with _251] the at Java. Security. SecureClassLoader. DefineClass (SecureClassLoader. Java: 142) ~ [? : 1.8.0 comes with _251] the at Java.net.URLClassLoader.defineClass URLClassLoader. Java: (468) ~ [? : 1.8.0 comes with _251] the at Java.net.URLClassLoader.access$100 URLClassLoader. Java: (74) ~ [? : 1.8.0 comes with _251] the at Java.net.URLClassLoader$1.run URLClassLoader. Java: (369) ~ [? : 1.8.0 comes with _251] the at Java.net.URLClassLoader$1.run URLClassLoader. Java: (363) ~ [? : 1.8.0 comes with _251] the at Java. Security. The AccessController. DoPrivileged (Native Method) ~ [? : 1.8.0 comes with _251] the at Java.net.URLClassLoader.findClass URLClassLoader. Java: (362) ~ [? : 1.8.0 comes with _251] the at Java. Lang. This. LoadClass (418). This Java: ~ [? : 1.8.0 comes with _251] the at Sun. Misc. The Launcher $AppClassLoader. LoadClass (355). The Launcher Java: ~ [? : 1.8.0 comes with _251] the at Java. Lang. This. LoadClass (405). This Java: ~ [? : 1.8.0 comes with _251] the at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:94) ~[study-minder.jar:?] The at Java. Lang. This. LoadClass (351). This Java: ~ [? : 1.8.0 comes with _251] the at Org. Springframework. Util. ClassUtils. Class.forname (ClassUtils. Java: 251) ~ [spring - core - 4.3.20. The jar! / : 4.3.20. RELEASE] the at...Copy the code

2. Dependencies

I. Background

  • By default, Java adds the JavaAgent JAR toAppClassLoaderIn the search path of
  • The structure of the JAR packaged by the Springboot plug-in, i.ejar-in-jar/nested-jars
  • The Springboot plugin packages the general process of jar startup and its useLaunchedURLClassLoader

See: Common JAR package form in Java and structure parsing

II. Class relationship and ClassLoader analysis of the error

The javaAgent jar package is called agent JAR. The JAR package packaged by Springboot is called springboot-Application JAR

Three, problem investigation

1.NoClassDefFoundErrorandClassNotFoundExceptionWhat was the mistake?

Note the difference between ClassNotFoundException, where a class was not found, and NoClassDefFoundError, where an error occurred during the class loading process (such as a static block of code that failed to execute).

So the translation error is: In loading com. Wingli. Agent. Helper. Util. SpringContextHolder, couldn’t find the org. Springframework. Context. The ApplicationContextAware class.

2.org.springframework.context.ApplicationContextAwareReally?

At this time Arthas will be sacrificed.You can clearly see that the class is loaded.

Then why is it loadingcom.wingli.agent.helper.util.SpringContextHolder“Will be reportedClassNotFoundException?

3. If an error occurs during class loading, what is done during class loading?

Because jar-in-jar/nested-jars uses a custom classloader and wraps the startup process, you need to add the POM of spring-boot-Loader to facilitate debugging

From the above error under caused as you can see, org. Springframework. Util. ClassUtils# class.forname place into the logic of class loading, then started playing conditional breakpoints here:

A. Entry class loader: LanuchedURLClassLoader

B. Delegate parent loader: AppClassLoader

C. Delegate parent loader: ExtClassLoader

D. Delegate parent loader: BoostrapClassLoader

E. boostrapClassLoader fails. ExtClassLoader loads the file automatically

F. extClassLoader fails. AppClassLoader loads the file automatically

Tips:AppClassLoader adds AgentJAR to the class search path by default

G. apppclassLoader can search for classes, butdefineClassfailed

DefineClass throws the above initial caused:ClassNotFoundException: org.springframework.context.ApplicationContextAware

4. Organize and analyze

As you can see from the class loading process above, the error is thrown because: AppClassLoader attempts to define (apart from Not Found) class com. Wingli. Agent. The helper. Util. SpringContextHolder, Can not find the org. Springframework. Context. ApplicationContextAware class.

5. WhyAppClassLoaderCan’t findorg.springframework.context.ApplicationContextAware?

. Org. Springframework. Context is spring ApplicationContextAware rely on in class, AppClassLoader can search only for Springboot-Application JAR and Agent JAR, as shown in the class diagram. Org. Springframework. Context. The ApplicationContextAware is in the spring – the context – 4.3.20. The jar, The jar is included jar in jar in springboot-Application jar, and the AppClassLoader does not add the JAR in jar URL to its class search path, so it cannot find the class.

6. What does Arthas see?

We saw this earlier using Arthas’s SC commandorg.springframework.context.ApplicationContextAwareIs it fake?

Of course not, only it’s notAppClassLoaderLoaded, but insteadLanuchedURLClassLoaderLoaded, the class loader isAppClassLoaderSubclass loader, soAppClassLoaderThis class cannot be used directly. usesc -d org.springframework.context.ApplicationContextAwareYou can see the detailed loading information

7. Why is there no problem with idea run?

Because idea does not use thespringboot-application jarThe way to launch the application, so it is not usedLanuchedURLClassLoader, there is no wrapper around the startup process, and the application’s own dependencies (such as Spring) andagent jarAll inAppClassLoaderDoes not exist in the pathjar in jarSo there are no parent-child classloaders.

Iv. Problem analysis

A picture is worth a thousand words:

V. Solutions

Now that the cause is clear, so is the solution! To avoid this problem, must distinguish between the class loader hierarchy need to load classes, because org. Springframework. Context. The ApplicationContextAware exists in springboot – application jar, And can only be loaded by LanuchedURLClassLoader, So the key to solve this problem is to make LanuchedURLClassLoader to load com. Wingli. Agent. Helper. Util. SpringContextHolder!

1. Separate dependencies[Pollution Code]

Separate the classes in JavaAgent that need to be loaded by Spring into a POM dependency, and then have the project add the dependency so that the dependency is packaged only in the SpringBoot-Application JAR, and only bytecode modification is done in JavaAgent. When the bean is triggered to load, it will all be loaded by LanuchedURLClassLoader without class loading problems.

Tips: This is actually necessary and reasonable if the bean you want to load needs to be used online!

2. Block the loading of some classes and break the parent delegate mechanism【 incomplete 】

Since the parent delegate mechanism would makeAppClassLoaderTry to load thecom.wingli.agent.helper.util.SpringContextHolderClass, then break this rule for this class! willagent jarAdded to theLanuchedURLClassLoaderThe search path of whenLanuchedURLClassLoaderencountercom.wingli.agent.helper.util.SpringContextHolderClass, directly by their own class load, do not go to the parent delegate. Hint:

Tips: com. Wingli. Agent. Helper. Util. SpringContextHolder for AppClassLoader is still visible!

3. Use it forcibly in javaAgentLanuchedURLClassLoaderLoading in advancecom.wingli.agent.helper.util.SpringContextHolder【 incomplete 】

This method is similar to the previous method, except that the loading place is placed in the JavaAgent logic, and there is no need to modify the classloader class search path, just use javassist.ctclass #toClass() to force the break of the parent delegate mechanism. Let LanuchedURLClassLoader loading com. Wingli. Agent. Helper. Util. SpringContextHolder class, due to the fact that class has been loaded will be cached, next time trigger loading, can directly read cache, will no longer trigger such searches, Nature does not go parental delegation. But com. Wingli. Agent. Helper. Util. SpringContextHolder to AppClassLoader also is still visible!

4. Separate dependencies and leveragejar in jarFeature controls classLoader visibility to dependencies

Taking advantage of the fact that AppClassLoader does not read jar in JAR classes, LanuchedURLClassLoader can read Jar in JAR classes, combined with proper separation of dependencies, can solve this problem more elegently.

Six, implementation steps

Javaagent: JavaAgent: javaAgent: javaAgent: JavaAgent: JavaAgent: JavaAgent: JavaAgent: JavaAgent: JavaAgent: JavaAgent One way is jar in jar (the way it will be implemented), and the Classloader needs to specify a path (a specially fetched URL) to read the dependency.

1. Modify the logic and dependency relationship in javaAgent

I. Schematic of the old implementation (all classes together)

II. New implementation diagram (subcontracting classes and dependencies to the expected visibility of the Classloader)

The Transform class is loaded by AppClassLoader, so its dependencies are loaded by AppClassLoader (even if the ContextLoader of the current thread is LaunchedURLClassLoader). Therefore, the Transform module cannot rely directly on the Helper module, but can only use reflection, or when driving piles.

2. Modify the JavaAgent packaging logic

I. Schematic diagram of the old JAR package (all classes and dependencies are decompressed into the top level directory of the JAR)

II. Schematic diagram of the new JAR package (subcontracted and adoptedjar in jarPut together)

Package the transform module as jar-with-dependencies, and the helper module as jar in jar into transform.jar.

3. Make helper.jar in transorm.jar pairLaunchedURLClassLoadervisible

Jar jar into tramsform.jar to make AppClassLoaer unable to load helper classes. Half the way, you need to make LaunchedURLClassLoader load Helper classes. A special URL for helper.jar needs to be generated somewhere in the code and added to the LaunchedURLClassLoader’s classloading path.

I. Selection of intercept points

  • This must be added before using classes in helper.jar
  • Add once only
  • The ClassLoader must beLaunchedURLClassLoader

This paper chooses:org.springframework.boot.SpringApplication, which is the Springboot application startup class

II. Code implementation

In load org. Springframework. Boot. When SpringApplication tranform call this method

private void appendAgentNestedJars(ClassLoader classLoader) {
    String agentJarPath = getAgentJarPath();
    if (agentJarPath == null) return;

    //LaunchedURLClassLoader is a springboot-loader class that is loaded by AppClassLoader
    if (classLoader instanceof LaunchedURLClassLoader) {
        LaunchedURLClassLoader launchedURLClassLoader = (LaunchedURLClassLoader) classLoader;
        try {
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            method.setAccessible(true);
            JarFileArchive = JarFileArchive = JarFileArchive = jar in jar
            JarFileArchive jarFileArchive = new JarFileArchive(new File(agentJarPath));
            List<Archive> archiveList = jarFileArchive.getNestedArchives(new Archive.EntryFilter() {
                @Override
                public boolean matches(Archive.Entry entry) {
                    if (entry.isDirectory()) {
                        return false;
                    }
                    return entry.getName().startsWith("BOOT-INF/lib/") && entry.getName().endsWith(".jar"); }});for (Archive archive : archiveList) {
                method.invoke(launchedURLClassLoader, archive.getUrl());
                System.out.println("add url to classloader. url:"+ archive.getUrl()); }}catch (Throwable t) {
            t.printStackTrace();
        }
    }


    System.out.println("trigger add urls to classLoader:" + classLoader.getClass().getName() + " agentJarPath:" + agentJarPath);

}
Copy the code

7. Verification of results

1. Final packaged JAR structure

2. Pile insertion

3.Class search path of ClassLoader

4. Visibility of ClassLoader to classes

  • LaunchedURLClassLoaderYou can load classes in the helper

  • AppClassLoaderClasses in a helper cannot be loaded

5.jar in jarThe Bean loading condition in

Eight, code,

1. Making the warehouse

Github.com/isadliliyin…

2. Branch Description:

  • Default-dependency is the older implementation
  • Split-dependency is the new implementation

-Leonard: Welcome to the END!