前言
类加载机制是Java领域的一个重要内容,包括热部署、框架、反射、动态代理,或多或少都和类加载有些相关
本篇记录一下通过学习类加载机制解决加载数据库驱动的问题。
一、问题的产生
之前八月份我试图基于Socket编写一个简单的HTTP服务器,实现了通过解析HTTP请求,返回响应的静态资源,或者通过反射执行响应的简单Java方法的功能。
反射调用简单的Java方法并没有问题,但是当我在项目中引入数据库之后,问题就出现了:ClassNotFoundException,找不到数据库驱动类。
于是我稍加思索,在项目中配置了mysql驱动类jar包的路径,然后再运行就可以连接数据库了。好!问题解决了!(并没有)
因为我的目标不是写一个网站后台程序,而是写一个通用型的服务器。作为一个服务器,如果写web应用还要在这个服务器项目内写的话,耦合度太高了,我希望能降低耦合度,将服务器和web应用分开来。只要写好xml配置文件,然后启动服务器,在xml文件里写的路径下存放web应用,就能运行,像是超级低配版的tomcat。
所以这使得在项目中修改.classpath文件(即配置路径)的方法无法使用。
我思来想去,觉得只能自定义类加载器来解决了。
二、解决的方法
1.继承URLClassLoader,重写findClass方法
这里有几个问题我还没有搞清楚:
1.URLClassLoader类的findClass方法不是空方法,他的作用是什么?
2.直接调URLClassLoader类的loadClass方法不行,调findClass为什么就可以?loadClass失败之后不是会调用findClass吗?(URLClassLoader没有重写loadClass方法,所以实际上调用的是ClassLoader的loadClass方法)
import java.net.URL;
import java.net.URLClassLoader;
public class MyLoader extends URLClassLoader{
//父类没有空的构造方法,所以子类必须定义构造方法
public MyLoader(URL[] urls) {
super(urls);
}
//父类的findClass是protected的,这里包装成public的方法以供实例调用
public Class<?> findClass(String name) throws ClassNotFoundException{
Class clz = super.findClass(name);
return clz;
}
}
2.创建类加载器
main方法
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 {
//这两个路径分别是我存放mysql驱动包的路径和建立数据库驱动的类所在的文件夹
URL url0 = new URL("file:///D:/springboot/下载项目/dragon/mysql-connector-java-8.0.21.jar");
URL url1 = new URL("file:///D:/springboot/下载项目/");
URL[] urls = {url0,url1};
MyLoader loader = new MyLoader(urls);
//下面设置线程上下文加载器这句代码没有用,后面会解释
Thread.currentThread().setContextClassLoader(loader);
//dragon是包名,不要奇怪,我就是喜欢龙
Class cls = loader.findClass("dragon.DBConn");
//通过自定义的Myloader类的findClass方法加载了DBConn类,下面就是反射调用方法了
Constructor constructor = cls.getConstructor();
Object obj = constructor.newInstance();
Method method = cls.getMethod("getConn");
method.invoke(obj);
}
}
数据库连接类的代码,虽然感觉没必要但我还是贴一下
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("数据库驱动加载成功");
}
catch(Exception e){
e.printStackTrace();
System.out.println("数据库驱动加载失败");
}
try {
Conn=DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/数据库名?serverTimezone=UTC","root","这里是密码");
}
catch(Exception e) {
e.printStackTrace();
System.out.println("数据库连接失败");
}
return Conn;
}
}
设置线程上下文加载器为什么没有用?
在ClassLoaderTest类中设置线程上下文加载器为loader,在DBConn类中的线程上下文加载器确实也是loader。但是,Class.forName(String)方法,他调的不是线程上下文加载器,而是当前类加载器。采用loadClass(String)方法或者Class.forName(String,boolean,ClassLoader)方法才可以调用线程上下文加载器。但是这两个方法,他们加载出的类是没有进行初始化的,也就是没有执行静态代码块,而数据库驱动是在静态代码块里初始化的,所以即使可以加载数据库驱动类,也连接不了数据库。
简单说明下forName(String)和forName(String,boolean,ClassLoader)方法的区别,我也不是很懂
@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;
上面是源码,可以看到,两个方法最后都是将参数传给forName0进行执行,forName0是个用C++编写的本地方法(至少HotSpot是这样,不过也有纯Java代码的JVM),什么用处我就不知道了。
但是可以看到,forName(String)传递给forName0的类加载器,是ClassLoader.getClassLoader(caller),Class类和ClassLoader类都有getClassLoader方法,这里ClassLoader的getClassLoader方法则调用了Class类的getClassLoader0方法(好绕啊. . .),这又是一个本地方法,我又不知道他什么作用了,大概就是返回“当前类加载器”吧,然后在把当前类加载器作为参数传给forName0。
而forName(String,boolean,ClassLoader)则会将指定的类加载器传给forName0,按理说只要布尔参数传入true,就应该也能初始化,但是不知道为什么我尝试的时候不行。
绕开双亲委派机制
如果遵循双亲委派机制,那么我们自定义的MyLoader会首先把DBConn类交给父加载器App类加载尝试加载,然后这一加载,AppClassLoader就觉得自己行了,然后就没MyLoader的事了。
???
你知道我把mysql驱动包放哪了???
他当然不知道,结果就是ClassNotFound异常,找不到com.mysql.cj.jdbc.Driver
继承URLClassLoader的MyLoader知道,因为我往里面传了mysql驱动包的路径,但是现在有双亲委派机制,没他什么事啊…
所以就必须想办法绕开双亲委派,这里我用的是绕开而不是破坏,因为我的代码并没有破坏双亲委派机制,我只是想办法绕开了他(笑)
我把DBConn类文件从bin目录(也就是classpath)下移除,放到了其他地方,然后把这个路径也传给MyLoader(这就是url1)
现在好了,AppClassLoader找不到了,只能让MyLoader来加载了,这就绕开了双亲委派。因为URLClassLoader可以通过多个路径去查找,所以MyLoader既可以找到DBConn类,也可以找到mysql驱动包
总结
还有很多没搞清楚的,等我明白了再更新修改。