The background,

The list management system is a system in which each module of the mobile phone configits the application to be controlled into a file and then delivers it to the mobile phone for application control, such as the power consumption control of each application. Considering security issues, the application files of each module have their own different encryption methods. According to past experience, we can use template method + factory mode to obtain different encryption methods according to the type of the module. The code class hierarchy is shown as follows:

Get the class structure diagram of different encryption methods

With the factory and template method patterns, we can satisfy the “close to change, open to extension” principle by adding new handlers when new encryption methods are available, but this approach inevitably requires code changes and re-releases and rollouts. So is there a better way to solve this problem? Here is the topic we are going to focus on today.

Second, class loading timing

When a type is loaded into vm memory and unloaded from memory, Its entire life cycle will go through Loading, Verification, Preparation, Resolution, and initialization Initialization, Using and Unloading. The three parts of Initialization, preparation and parsing are collectively known as Linking. The sequence of these seven stages is shown in Figure 1.

Although the classloader loading process has a complex 7 steps, the fact that all but four of these steps are controlled by the JVM doesn’t leave much room for intervention beyond developing to fit its specifications. Loading is the most important way we control the ClassLoader for a particular purpose. This is also the focus of our introduction.

Three, load,

The “Loading” stage is a stage in the whole “Class Loading” process. During the load phase, the Java virtual machine needs to do three things:

  • Gets the binary byte stream that defines a class by its fully qualified name.

  • Transform the static storage structure represented by this byte stream into the runtime data structure of the method area.

  • Generate a java.lang.Class object in memory that represents the Class and acts as an access point for the Class’s various data in the method area.

The Java Virtual Machine Specification has no specific requirements for these three points, leaving considerable flexibility for both virtual machine implementations and Java applications. For example, the rule “get the binary byte stream that defines a Class by its fully qualified name” does not specify that the binary byte stream must be obtained from a Class file, or exactly where or how to get it at all. For example, we can read from a ZIP package, get it from the network, compute it at runtime, generate it from other files, and read it from a database. It can also be obtained from an encrypted file.

From here we can see that as long as we can get the. Class file of the encryption class, we can get the corresponding encryption class object through the class loader, and then call the specific encryption method through reflection. Class loaders therefore play a crucial role in the loading process of.class files.

4. Parental delegation model

Currently, there are three types of Java virtual machine loaders: startup class loader, extension class loader, and application class loader. Most Java programs load using these three types of loaders.

4.1 Starting the class loader

This class, implemented by C++, is responsible for loading files stored in the \lib directory, or in the path specified by the -xbootclasspath parameter, and is recognized by the Java virtual machine (by filename, e.g. Rt.jar, tools.jar, Libraries with incorrect names will not be loaded even if placed in the lib directory.) The libraries are loaded into the virtual machine memory. The boot class loader cannot be directly referenced by Java programs. If you need to delegate the loading request to the boot class loader when writing a custom class loader, you can use NULL instead.

4.2 Extended class loaders

The classloader is implemented as Java code in the sun.misc.Launcher$ExtClassLoader class. It is responsible for loading all libraries in the \lib\ext directory, or in the path specified by the java.ext.dirs system variable. The name “extension classloader” implies that this is a mechanism for extending Java system libraries. The JDK development team allowed users to place general-purpose libraries in ext directories to extend Java SE functionality. After JDK9, this mechanism was replaced by the natural extension capabilities of modularity. Since the extension classloader is implemented in Java code, developers can use the extension classloader directly in their programs to load Class files.

4.3 Application class loader

The classloader is implemented by sun.misc.Launcher$AppClassLoader. Because the application ClassLoader is the return value of the getSystemClassLoader() method in the ClassLoader class, it is also called the “system ClassLoader” in some cases. It is responsible for loading all libraries on the user’s ClassPath, and developers can use the class loader directly in their code. If the application does not have its own custom class loader, this is generally the default class loader in the application.

Since the existing classloaders have special loading paths, the. Class files generated by the encrypted classes compiled by ourselves are not stored in the paths of the three existing classloaders, so it is necessary to define our own classloaders.

Custom class loaders

All class loaders except the root ClassLoader are subclasses of ClassLoader. So we can implement our own ClassLoader by inheriting ClassLoader.

The ClassLoader class has two key methods:

  • Protected Class loadClass(String name, Boolean resolve) : Name is the Class name. If resove is true, the Class will be resolved at load time.

  • Protected Class findClass(String name) : Finds the Class by the specified Class name.

So, if you want to implement custom classes, you can override these two methods to do so. But it is recommended to override the findClass method rather than the loadClass method, which might break the parent-delegate model of class loading because the findClass method is called internally by the loadClass method.

protectedClass<? > loadClass(String name,boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loadedClass<? > c = findLoadedClass(name);if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if(parent ! =null) {
                        c = parent.loadClass(name, false);
                    } else{ c = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
 
                    // this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }}if (resolve) {
                resolveClass(c);
            }
            returnc; }}Copy the code

LoadClass load method flow:

  • Determine whether this class has been loaded;

  • If the parent loader is not null, the parent loader is used for loading. Instead, use the root loader to load;

  • If none of the previous loads succeeded, the findClass method is used to load.

Therefore, in order not to affect the class loading process, we rewrite findClass method can easily implement custom class loading.

Six, code implementation

6.1 Implementing a custom class loader

public class DynamicClassLoader extends ClassLoader {
 
    private static final String CLASS_EXTENSION = "class";
 
    @Override
    publicClass<? > findClass(String encryptClassInfo) { EncryptClassInfo info = JSON.parseObject(encryptClassInfo, EncryptClassInfo.class); String filePath = info.getAbsoluteFilePath(); String systemPath = System.getProperty("java.io.tmpdir");
        String normalizeFileName = FilenameUtils.normalize(filePath, true);
        if(StringUtils.isEmpty(normalizeFileName) || ! normalizeFileName.startsWith(systemPath) ||getApkFileExtension(normalizeFileName) ==null| |! CLASS_EXTENSION.equals(getApkFileExtension(normalizeFileName))) {return null;
        }
 
        String className = info.getEncryptClassName();
        byte[] classBytes = null;
        File customEncryptFile = new File(filePath);
        try {
            Path path = Paths.get(customEncryptFile.toURI());
            classBytes = Files.readAllBytes(path);
        } catch (IOException e) {
            log.info("Encryption error", e);
        }
        if(classBytes ! =null) {
            return defineClass(className, classBytes, 0, classBytes.length);
        }
        return null;
    }
 
    private static String getApkFileExtension(String fileName) {
        int index = fileName.lastIndexOf(".");
        if(index ! = -1) {
            return fileName.substring(index + 1);
        }
        return null; }}Copy the code

This is mainly through the integration of ClassLoader, copy findClass method, from the encryption class information to obtain the corresponding. Class file information, finally obtain the encryption class object

The encrypt() method in the 6.2.class file

public String encrypt(String rawString) {
        String keyString = "R.string.0x7f050001";
        byte[] enByte = encryptField(keyString, rawString.getBytes());
        return Base64.encode(enByte);
    }
Copy the code

6.3 Specific Invocation

public class EncryptStringHandler {
 
    private static finalMap<String, Class<? >> classMameMap =new HashMap<>();
 
    @Autowired
    private VivofsFileHelper vivofsFileHelper;
 
    @Autowired
    private DynamicClassLoader dynamicClassLoader;
 
    public String encryptString(String fileId, String encryptClassName, String fileContent) {
        try{ Class<? > clazz = obtainEncryptClass(fileId, encryptClassName); Object obj = clazz.newInstance(); Method method = clazz.getMethod("encrypt", String.class);
            String encryptStr = (String) method.invoke(obj, fileContent);
            log.info("The original string is :{}, the encrypted string is :{}", fileContent, encryptStr);
            return encryptStr;
        } catch (Exception e) {
            log.error("Custom loader loading encryption class exception", e);
            return null; }}privateClass<? > obtainEncryptClass(String fileId, String encryptClassName) { Class<? > clazz = classMameMap.get(encryptClassName);if(clazz ! =null) {
            return clazz;
        }
 
        String absoluteFilePath = null;
        try {
            String domain = VivoConfigManager.getString("vivofs.host");
            String fullPath = domain + "/" + fileId;
            File classFile = vivofsFileHelper.downloadFileByUrl(fullPath);
            absoluteFilePath = classFile.getAbsolutePath();
            EncryptClassInfo encryptClassInfo = new EncryptClassInfo(encryptClassName, absoluteFilePath);
            String info = JSON.toJSONString(encryptClassInfo);
            clazz = dynamicClassLoader.findClass(info);
            // Set the cache
            Assert.notNull(clazz, "Custom class loader loading encryption class exception");
            classMameMap.put(encryptClassName, clazz);
            return clazz;
        } finally {
            if(absoluteFilePath ! =null) {
                FileUtils.deleteQuietly(newFile(absoluteFilePath)); }}}}Copy the code

Through the implementation of the above code, we can add a compiled.class file in the management platform, and finally through the custom class loader and reflection call method, to achieve the specific method call, avoiding the need to modify the code and re-issue to adapt to the problem of constantly adding encryption methods.

Seven, problem,

The above code was tested locally without any exceptions, but when deployed to the test server, a JSON parsing exception occurred that appeared to be a JSON string formatted incorrectly.

Json parsing logic is used to convert a string to an object at the entrance to the DynamicClassLoader#findClass method.

Found in addition to what we need right here in ginseng (printing) first into the reference information, more than a Base64 comspec cn. Hutool. Core. The codec. Base64. The Base64 loadClass method is called loadClass. The Base64 loadClass method is called loadClass. The Base64 loadClass method is called loadClass. Because findClass has been overwritten, the json parsing error is reported.

protectedClass<? > loadClass(String name,boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loadedClass<? > c = findLoadedClass(name);if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if(parent ! =null) {
                        c = parent.loadClass(name, false);
                    } else{ c = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
 
                    // this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }}if (resolve) {
                resolveClass(c);
            }
            returnc; }}Copy the code

However, we do not want any classes to be loaded with custom class loaders except for the encrypted. Class files. By analyzing the ClassLoader#loadClass method, we want to be able to load the Base64 tripartite class through its parent class loader. Since Bootstrap Class Loader cannot be loaded into Base64, we need to display the setting of the parent Class Loader. However, we need to understand the Structure of the Tomcat Class Loader.

The main reason why Tomcat needs a class loading structure on top of the JVM is to solve the following problems:

  • Java libraries used by two Web applications deployed on the same server can be isolated from each other;

  • Java libraries used by two Web applications deployed on the same server can be shared;

  • The server needs to keep itself as secure as possible. The class library used by the server should be independent of the application class library.

  • Most Web servers that support JSP applications need to support HotSwap.

To this end, Tomcat extends the CommonClass loader, CatalinaClassLoader, SharedClassLoader, and WebApp class loader PClassLoader), which loads the logic of Java libraries in/Commons /, /server/, /shared/* and /WebApp/WEB-INF/* respectively.

The WebAppClassLoader can load dependencies in /WEB-INF/*. And we rely on class cn. Hutool. Core. The codec. The Base64’s package hutool – all – 4.6.10 – sources. The jar is in/WEB – under the INF / * directory, Jar is also in/web-INF /*, so the DynamicClassLoader is also loaded by WebAppClassLoader.

We can write a test class to test:

@Slf4j
@Component
public class Test implements ApplicationListener<ContextRefreshedEvent> {
 
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("classLoader DynamicClassLoader:"+ DynamicClassLoader.class.getClassLoader().toString()); }}Copy the code

Test results:

So we can set the parent of the DynamicClassLoader to be the class loader that loads itself:

public DynamicClassLoader(a) {
        super(DynamicClassLoader.class.getClassLoader());
}
Copy the code

When we perform the file encryption and decryption operation again, no errors have been found, and by adding logs, We can see that the class is loaded cn. Hutool. Core. The codec. The corresponding class loader for loading indeed Base64 DynamicClassLoader corresponding class loader WebAppClassLoader.

public String encrypt(String rawString) {
        log.info("classLoader Base64:{}", Base64.class.getClassLoader().toString());
        String keyString = "R.string.0x7f050001";
        byte[] enByte = encryptField(keyString, rawString.getBytes());
        return Base64.encode(enByte);
    }
Copy the code

Now think about, why don’t under the IDEA of running environment need to set a custom class loader of the parent class loader can be loaded into the cn. Hutool. Core. The codec. Base64.

Add the following information in the IDEA running environment:

public String encrypt(String rawString) {
        System.out.println("Class loader details...");
        System.out.println("classLoader EncryptStrategyHandler:" + EncryptStrategyHandlerH.class.getClassLoader().toString());
        System.out.println("classLoader EncryptStrategyHandler:" + EncryptStrategyHandlerH.class.getClassLoader().getParent().toString());
        String classPath = System.getProperty("java.class.path");
        System.out.println("classPath:" + classPath);
        System.out.println("classLoader Base64:" + Base64.class.getClassLoader().toString());
        String keyString = "R.string.0x7f050001";
        byte[] enByte = encryptField(keyString, rawString.getBytes());
        return Base64.encode(enByte);
    }
Copy the code

DynamicClassLoader is the custom DynamicClassLoader, and the parent of the.class loader is the AppClassLoader. Load the cn. Hutool. Core. The codec. Class loader is AppClassLoader Base64.

The specific loading process is as follows:

1) Delegate the custom class loader to the AppClassLoader;

2) Delegate the AppClassLoader to the parent ExtClassLoader;

3) The ExtClassLoader delegates to the BootStrapClassLoader, but the BootClassLoader fails to load the ExtClassLoader, so the ExtClassLoader itself fails to load the ExtClassLoader.

4) Load by AppClassLoader;

AppClassLoader calls its parent UrlClassLoader’s findClass method to load the AppClassLoader.

5) from the user class path eventually Java class. The path is loaded into the cn. Hutool. Core. The codec. Base64.

As a result, we found that under the IDEA of environment, custom encryption class. A class file depends on the three parties of cn. Hutool. Core. The codec, is can be loaded through AppClassLoader Base64.

And under the Linux environment, through the remote debugging, found that when the initial loading cn. Hutool. Core. The codec. The class loader for DynamicClassLoader Base64. Then, the parent class loader is delegated to AppClassLoader for loading. According to the principle of parental delegation, the parent class loader will be handed over to AppClassLoader for processing. But still did not find the class under the user’s path cn. Hutool. Core. The codec. Base64, eventually to DynamicClassLoader to load, finally appeared at the beginning of the JSON parse error.

Eight, summary

Since the class loading phase does not strictly restrict how to obtain the binary stream of a class, it provides the possibility of code extensibility by dynamically loading.class files through custom class loaders. Flexibility in customizing classloaders can also play an important role in other areas, such as implementing code encryption to avoid core code leaks, resolving conflicts caused by different services relying on different versions of the same package, and implementing hot deployment to avoid frequent application restarts during debugging.

Ix. Reference materials

Understanding the Java Virtual Machine in Depth: Advanced JVM Features and Best Practices (version 3)

2, The most powerful ever -Java class loader principle and application

Vivo Internet Server Team -Wang Fei