Java架构直通车——以JDBC为例谈双亲委派模型的破坏

引入

java给数据库操作提供了一个Driver接口:

public interface Driver {
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

然后提供了一个DriverManager来管理这些Driver的具体实现:

public class DriverManager {
    // List of registered JDBC drivers 这里用来保存所有Driver的具体实现
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        registerDriver(driver, null);
    }

    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {
        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }
        println("registerDriver: " + driver);
    }
}

这里省略了大部分代码,可以看到我们使用数据库驱动前必须先要在DriverManager中使用registerDriver()注册,然后我们才能正常使用。

JDBC4.0之前

在JDBC4.0之前,我们看下mysql的驱动是如何被加载的:

         // 1.加载数据访问驱动
        Class.forName("com.mysql.jdbc.Driver");
        //2.连接到数据"库"上去
        Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

核心就是这句Class.forName()触发了mysql驱动的加载,我们看下mysql对Driver接口的实现:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

可以看到,通过Class.forName(),其实触发了类加载,我们知道类加载机制分为:
加载->验证->准备->解析->初始化

在初始化阶段,通过反射也可以触发类的初始化,类初始化了,也就是出发了Driver类的静态代码块,通过这个静态代码库向DriverManager中注册了一个mysql的Driver实现。这个时候,我们通过DriverManager去获取connection的时候只要遍历当前所有Driver实现,然后选择一个建立连接就可以了。

JDBC4.0之后

在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver文件中指明当前使用的Driver是哪个。

然后使用的时候就直接这样就可以了:

 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

那么为什么JDBC4.0之后,就不需要显性去加载这个驱动了,难道rt.jar 里的DriverManager可以自主注册驱动包么,当然这是不可能的。

为了解决这个问题,jdk采用了上下文加载。

在你调用 DriverManager.getConnection方法时,会加载DriverManager类并执行他的静态方法:

	static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

我们进入该方法loadInitialDrivers()

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
               	try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }

其中ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class)就是上下文加载,其中的ServiceLoader 即为上下文加载工具包。

当代码执行到:
在这里插入图片描述
就会调用:
在这里插入图片描述
如你所见,其实他也是去使用Class.forName 去加载这个类,而且他还带了一个加载器的参数。

我们知道,如果单纯只用Class.forName()(不带参数)加载用的是调用者的Classloader,在JDBC4.0之前是不带参数的,也就是说,这里的调用者其实就是应用类加载器AppClassLoader,是可以向上委托进行加载的(虽然向上委托找不到Driver类,最终还是由应用类加载器自己加载)。

而如果是在DriverManager中单纯只用Class.forName()(不带参数)加载,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,也就是说由启动类加载器进行加载,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,启动类加载器无法加载。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

那么应该怎么办呢?这个mysql的drvier 只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器,也就是利用上下文加载的方式传递过来的AppClassloader进行加载。

这种自动加载采用的技术叫做SPI。很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则。

SPI机制的约定:
在META-INF/services/目录中创建以接口全限定名命名的文件该文件内容为Api具体实现类的全限定名.
使用ServiceLoader类动态加载META-INF中的实现类,如SPI的实现类为Jar则需要放在主程序classPath中.
Api具体实现类必须有一个不带参数的构造方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值