自定义类加载器加载数据库驱动


前言

类加载机制是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驱动包



总结

还有很多没搞清楚的,等我明白了再更新修改。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值