背景
类加载机制
最近在做一个新项目,项目中需要实现多种数据源执行SQL语句。由于数据源的来源多样,并且同一种数据源存在驱动包版本不一样的情况,所以打算将驱动包外置。即,将不同类型数据源的驱动包放在系统某一个路径下,在程序中根据用户配置的驱动包的路径、驱动包的驱动类名来获取一个数据库连接。
功能实现
1.先来看下最常用的代码实现:
/** step1: 首先加载oracle的驱动类
step2: 初始化的驱动类,在静态代码块中,将驱动注册进DriverManager里
**/
Class.forName("oracle.jdbc.OracleDriver");
// 获取数据库连接
Connection conn = DriverManager.getConnection(jdbcUrl, getUsername(), getPassword());
以上就是最常用的获取连接的方法。
第一句 Class.forName 方法加载驱动的源码如下,通过反射获取调用的类,然后从调用类的classloader 中获取该驱动类,随后初始化该类(initialize = true)。
因为在 JDBC 规范中明确要求 Driver (数据库驱动)类必须向 DriverManager 注册自己。所以驱动类在静态代码块中实现主动注册到 DriverManager 的逻辑,这样在初始化该类时就完成驱动的注册了。可参考以下 OracleDriver 有个直观的认知。
2.实现加载外置驱动jar包
结合常用的使用方法。那根据我的需求,我第一时间想到使用 URLClassLoader 加载相应的 driverClass。然后创建driverClass的实例,并注册到 DriverManager中,最后之间获取Connection就可以实现需求了。实现如下:
URL[] urls = new URL[]{new URL("file:D:\\drivers\\ojdbc8_19.3.jar)};
URLClassLoader classLoader = new URLClassLoader(urls);
Class<?> driverClass = classLoader.loadClass("oracle.jdbc.driver.OracleDriver");
Driver driver = (Driver) driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver(driver);
Connection conn = DriverManager.getConnection(jdbcUrl, getUsername(), getPassword());
这个实现真的可以吗?很多其他博客也是这么写的,但事实上,答案是否定的。如果纯外部引用,这样的写法会报错:
java.sql.SQLException: No suitable driver found for jdbc:oracle:thin:@localhost:1521:ORCL
跟踪到 DriverManager.getConnection 的源码里:
debug模式发现,虽然 registeredDrivers 里已经存在了我们从外部jar包中加载的驱动实例,但是却在获取连接之前被 isDriverAllowed 方法拒绝了。
这个方法也很简单,就是从类加载器中加载驱动类。那为什么报错了?!
答案就是这个类加载器的问题,此类加载器是调用 DriverManger.getConnection 方法的类的加载器,那肯定是获取不到的呀!
详细原因:
调用 DriverManger.getConnection 的类的类加载器是默认的 AppClassLoader。
在Java的类加载器双亲委派模型中,在默认情况下,会先委托给父类加载器( ExtClassLoader )来加载类,如果父类加载器无法加载(例如,类不在扩展类库中),委托给启动类加载器(Bootstrap ClassLoader)。只有启动类加载器找不到这个类时,扩展类加载器才会尝试加载。若扩展类加载器也无法加载,最后才会由应用程序类加载器 AppClassLoader 尝试从应用程序的类路径中加载。 但是,AppClassLoader 是负责加载应用程序类路径(包括Maven依赖的类库)中的类。而我们外置的驱动包里的类并不在他的范围内,当然就获取不到了。
我立马就想到了,那用DriverManager获取connection的时候,换成 URLClassLoader不就行了。很遗憾,这个方法没开放。
以上就是我的一个完整的思考过程。话不多说,最后上正确答案。
URL[] urls = new URL[]{new URL("file:" + oracleConnectionParam.getDriverLocation())};
URLClassLoader classLoader = new URLClassLoader(urls);
Class<?> driverClass = classLoader.loadClass(oracleConnectionParam.getDriverClassName());
Driver driver = (Driver) driverClass.getDeclaredConstructor().newInstance();
Properties connectionProperties = new Properties();
connectionProperties.put("user", oracleConnectionParam.getUser());
connectionProperties.put("password", oracleConnectionParam.getPassword());
Connection connection = driver.connect(oracleConnectionParam.getJdbcUrl(), connectionProperties);
直接用从 URLClassLoader 中加载的驱动类的 Driver 的实例获取数据库驱动即可。
小结
类加载器
小小的一个数据库连接获取的问题引发我对类加载器的深度思考,值了。不要放过任何一个小问题哦。