我们可以用自定义的 URLClassLoader 从外部动态加载类,并使用它。但数据库驱动的管理类 DriverManager 却不比较苛刻,不承认非当前应用系统加载器加载的驱动类。见 DriverManager 的 JavaDoc
When the method
getConnection
is called, theDriverManager
will attempt to locate a suitable driver from amongst those loaded at initialization and those loaded explicitly using the same classloader as the current applet or application
对于有有应用自定义类加载器加载数据库驱动类的需求时,就要对原 Driver 简单包装一下。继续往后会说介绍为什么要这么做。
说明一下,DriverManager 能够根据 JDBC 连接字符串匹配到驱动类,所以一般来说都不需要显式调用 DriverManager.registerDriver() 方法。
先看 DriverManager 在应用外部驱动类时会出现什么情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package cc.unmi; public class JdbcDriverLoader { public static void main(String[] args) { notWork(); } public static void notWork() throws Exception { URL url = new URL("file:~/drivers/mysql-connector-java-5.1.43.jar"); String driverClass = "com.mysql.jdbc.Driver"; URLClassLoader classLoader = new URLClassLoader(new URL[] {url}); Class.forName(driverClass, true, classLoader); DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useSSL=false", "root", ""); } } |
上面的代码在执行最后一行获取数据库连接时报出的异常是
java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/test?useSSL=false
但是能正确用自定义的类加载器加载到驱动类 com.mysql.jdbc.Driver, 否则会报出 ClassNotFound 的异常。把上面代码中的
Class.forName(driverClass, true, classLoader)
改成
Driver driver = (Driver) Class.forName(driverClass, true, classLoader).newInstance();
DriverManager.registerDriver(driver);
也无济于事。但是只要是系统加载器的数据库驱动就没问题,下面执行命令正常
java -cp ~/drivers/mysql-connector-java-5.1.43.jar cc.unmi.JdbcDriverLoader
到底发生了什么呢?还是那个 DriverManager, 进到它的 getConnection(....) 方法,有兴趣的可以去阅读 DriverManager 类的源代码,这里不细究的,简单来讲就是
在获取连接时,DriverManager 会用加载 cc.unmi.JdbcDriverLoader 类的加载器(Launcher$AppClassLoader)检验一下是否能加载到数据库驱动类,显然这里要卡壳了。com.mysql.jdbc.Driver 对于应用程序类加载器是不可能见的,所以报出驱动找不到的异常。
为了解决能动态的加载外部数据库驱动,我们需要引入下面那个 DriverShim 包装类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import java.sql.*; class DriverShim implements Driver { private Driver driver; DriverShim(Driver d) { this.driver = d; } public boolean acceptsURL(String u) throws SQLException { return this.driver.acceptsURL(u); } public Connection connect(String u, Properties p) throws SQLException { return this.driver.connect(u, p); } public int getMajorVersion() { return this.driver.getMajorVersion(); } public int getMinorVersion() { return this.driver.getMinorVersion(); } public DriverPropertyInfo[] getPropertyInfo(String u, Properties p) throws SQLException { return this.driver.getPropertyInfo(u, p); } public boolean jdbcCompliant() { return this.driver.jdbcCompliant(); } public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } } |
然后加从外部加载驱动时应该是 DriverShim 包装类型
1 2 3 4 5 6 7 8 |
public static void notWork() throws Exception { URL url = new URL("file:~/drivers/mysql-connector-java-5.1.43.jar"); String driverClass = "com.mysql.jdbc.Driver"; URLClassLoader classLoader = new URLClassLoader(new URL[] {url}); Driver driver = (Driver) Class.forName(driverClass, true, classLoader).newInstance(); DriverManager.registerDriver(new DriverShim(driver)); DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useSSL=false", "root", ""); } |
这样就行了,可以正确找到 "com.mysql.jdbc.Driver", 为什么如此简单的包装就把骄傲的 DriverManager 给骗了呢?就这么简单。
使用了 DriverShim 包装类后,在 getConnection() 时 DriverManager 同样要验证驱动是否对应用程序类加载器可见,只是这时候要验证的是这个 DriverShim 而非通过自定义类加载器弄进来的 "com.mysql.jdbc.Driver" 了。而后在使用 DriverShim 桥接到实际的 com.mysql.jdbc.Driver 时 DriverManager 就管不着了。
大致意思就是:你 DriverManager 不是想验证数据库驱动是否是应用程序类加载器加载的吗?给个壳逗你玩一下,我在壳里面呢,你管我是由哪个类加载器加载的。
[…] 先看 DriverManager 在应用外部驱动类时会出现什么情况 阅读全文 >> […]
这个思路不错。
成功解决了我的问题,赞啊
厉害了
[…] 先看 DriverManager 在应用外部驱动类时会出现什么情况 阅读全文 >> […]
嗯,这个有点意思。我前面做ANDROID开发时,想连接一下MYSQL库,结果居然没有办法。要加载的东西太多。有了你这篇文章,我倒是可以试一下看能否搞定了。