The custom class loader loads the database driver

Keywords: Java MySQL

preface

Class loading mechanism is an important content in the Java field, including hot deployment, framework, reflection and dynamic proxy, which are more or less related to class loading

This article records how to solve the problem of loading database driver through learning class loading mechanism.

1, Problem generation

In August, I tried to write a simple HTTP server based on Socket to realize the function of parsing HTTP requests, returning static resources of responses, or executing simple Java methods of responses through reflection.

There is no problem with reflection calling simple Java methods, but after I introduced the database into the project, the problem occurred: ClassNotFoundException. The database driver class cannot be found.

So I thought a little, configured the path of mysql driver class jar package in the project, and then run it to connect to the database. OK! The problem is solved! (no)

Because my goal is not to write a website background program, but to write a general server. As a server, if you write web applications in this server project, the coupling degree is too high. I hope to reduce the coupling degree and separate the server and web applications. As long as the xml configuration file is written, then the server is started, and the web application is stored under the path written in the xml file, it can run, such as the super low configuration version of tomcat.

Therefore, this makes it impossible to modify the. classpath file (that is, the configuration path) in the project.
After thinking about it, I think I can only customize the class loader to solve it.

2, Solution

1. Inherit URLClassLoader and override findClass method

Here are a few questions I haven't figured out yet:
1. The findClass method of urlclassloader class is not empty. What is its function?
2. If you can't directly call the loadClass method of URLClassLoader class, why can you just call findClass? Will findClass be called after loadClass fails? (URLClassLoader does not override the loadClass method, so it actually calls the loadClass method of ClassLoader)

import java.net.URL;
import java.net.URLClassLoader;
public class MyLoader extends URLClassLoader{
	//The parent class has no empty constructor, so the child class must define a constructor
	public MyLoader(URL[] urls) {
		super(urls);
	}
	//The findClass of the parent class is protected, which is packaged as a public method for instance calls
	public Class<?> findClass(String name) throws ClassNotFoundException{
		Class clz = super.findClass(name);
		return clz;
	}	
}

2. Create class loader

main method

package dragon;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
public class ClassLoaderTest {
	public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
		//These two paths are the path where I store the mysql driver package and the folder where I create the database driver class
		URL url0 = new URL("file:///D:/springboot / download project / dragon/mysql-connector-java-8.0.21.jar ");
		URL url1 = new URL("file:///D:/springboot / download project / ");
		URL[] urls = {url0,url1};
		MyLoader loader = new MyLoader(urls);
		//The following code to set the thread context loader is useless, which will be explained later
		Thread.currentThread().setContextClassLoader(loader);
		//dragon is the package name. Don't be surprised. I just like dragons
		Class cls = loader.findClass("dragon.DBConn");
		//The DBConn class is loaded through the findClass method of the custom Myloader class. The following is the reflection calling method
		Constructor constructor = cls.getConstructor();
		Object obj = constructor.newInstance();
		Method method = cls.getMethod("getConn");
		method.invoke(obj);
	}
}

Database connection class code, although I feel it is not necessary, I still post it

package dragon;
import java.sql.*;
public class DBConn {
	public static Connection Conn;
	public static Connection getConn() {
		try {
			Class clz = Class.forName("com.mysql.cj.jdbc.Driver");
			System.out.println("Database driver loaded successfully");
		}
		catch(Exception e){
			e.printStackTrace();
			System.out.println("Database driver loading failed");
		}
		try {
			Conn=DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306 / database name? serverTimezone=UTC","root "," here is the password ");
		}
		catch(Exception e) {
			e.printStackTrace();
			System.out.println("Database connection failed");
		}
		return Conn;
	}
}

Why is it useless to set the thread context loader?
Set the thread context loader in ClassLoaderTest class as loader, and the thread context loader in DBConn class is indeed a loader. However, the Class.forName(String) method does not call the thread context loader, but the current class loader. Use the loadClass(String) method or Class.forName(String,boolean,ClassLoader) Method can call the thread context loader. However, the classes loaded by these two methods are not initialized, that is, they do not execute the static code block, and the database driver is initialized in the static code block. Therefore, even if the database driver class can be loaded, it cannot connect to the database.

I don't quite understand the difference between forName(String) and forName(String,boolean,ClassLoader) methods

	@CallerSensitive
	public static Class<?> forName(String className)
                throws ClassNotFoundException {
    	Class<?> caller = Reflection.getCallerClass();
    	return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
	}
	@CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (loader == null) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (ccl != null) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }
	private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

The above is the source code. You can see that the two methods finally pass parameters to forName0 for execution. forName0 is a local method written in C + + (at least HotSpot is like this, but there is also a JVM with pure Java code). I don't know what use it is.
However, you can see that the Class loader passed from forName(String) to forName0 is ClassLoader.getClassLoader(caller). Both Class and ClassLoader classes have getClassLoader methods. Here, the getClassLoader method of ClassLoader calls the getClassLoader0 method of Class class (it's easy to get around...) , this is another local method. I don't know what its role is. It is probably to return the "current Class loader" and pass the current Class loader as a parameter to forName0.
While forName(String,boolean,ClassLoader) will pass the specified class loader to forName0. Normally, as long as the Boolean parameter is passed in true, it should also be initialized, but I don't know why I can't do it when I try.

Bypass the parental delegation mechanism
If the two parent delegation mechanism is followed, our customized MyLoader will first give the DBConn class to the parent loader to load the App class and try to load it. After this loading, the AppClassLoader will feel that it is OK, and then there will be no MyLoader.

???

Do you know where I put the mysql driver package???
Of course, he doesn't know. The result is a ClassNotFound exception. com.mysql.cj.jdbc.Driver can't be found
MyLoader, who inherits URLClassLoader, knows that because I passed the path of mysql driver package to it, but now there is a parental delegation mechanism. It's nothing for him
So I have to find a way to bypass the parental delegation. Here I use to bypass rather than destroy it, because my code does not destroy the parental delegation mechanism, I just try to bypass it (laughter)

I removed the DBConn class file from the bin directory (i.e. classpath), put it elsewhere, and then passed the path to MyLoader (this is url1)

Now, AppClassLoader can't find it, so we can only let MyLoader load it, which bypasses the parental delegation. Because URLClassLoader can find it through multiple paths, MyLoader can find both DBConn class and mysql driver package

summary

There are a lot of things I don't understand. I'll update and modify them when I understand.

Posted by jdsflash on Wed, 29 Sep 2021 15:21:43 -0700