聊聊JDBC是如何破坏双亲委派机制的

参考:https://www.jianshu.com/p/09f73af48a98

Java本身有一套资源管理服务JNDI,是SUN公司提供的一种标准的Java命名系统接口,是放置在rt.jar包中,由启动类加载器加载的,我们这里主要讲解的是JDBC。

原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的

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;
}

然后Java有一个DriverManager类来管理所有的加载的Driver驱动

public class DriverManager {


    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    /* Prevent the DriverManager class from being instantiated. */
    private DriverManager(){}

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    @CallerSensitive
    public static Connection getConnection(String url,
        java.util.Properties info) throws SQLException {

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

    @CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

    @CallerSensitive
    public static Connection getConnection(String url)
        throws SQLException {

        java.util.Properties info = new java.util.Properties();
        return (getConnection(url, info, Reflection.getCallerClass()));
    }

 //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // 验证当前获取连接的类的类加载器和当前驱动的类加载器是不是同一个
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }

    @CallerSensitive
    public static Driver getDriver(String url)
        throws SQLException {

        println("DriverManager.getDriver(\"" + url + "\")");

        Class<?> callerClass = Reflection.getCallerClass();

        // Walk through the loaded registeredDrivers attempting to locate someone
        // who understands the given URL.
        for (DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerClass)) {
                try {
                    if(aDriver.driver.acceptsURL(url)) {
                        // Success!
                        println("getDriver returning " + aDriver.driver.getClass().getName());
                    return (aDriver.driver);
                    }

                } catch(SQLException sqe) {
                    // Drop through and try the next driver.
                }
            } else {
                println("    skipping: " + aDriver.driver.getClass().getName());
            }

        }

        println("getDriver: no suitable driver");
        throw new SQLException("No suitable driver", "08001");
    }
    
    // 验证当前驱动的类加载器是否是给定的类加载器
    private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }

             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }

}

省略了部分代码,从中我们可以看出,我们获取连接的时候,是到registeredDrivers中去查找对应的Driver来获取连接,Driver是需要先向DriverManager中进行注册的。

还可以看出,获取连接的时候为不同的类加载器加载的驱动进行了区分,当前类加载器加载的驱动只能有当前类加载器加载的类获取到,这是为了应用之间驱动版本的隔离(tomcat),因为我们不仅仅可以让SPI自动加载驱动,我们还可以自行注册驱动向DriveManager中。

以前我们使用JDBC的方法(未破坏双亲委派机制)

Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "1234");

我们手动的使用Class.forName()进行驱动类的加载,类加载器是使用的调用当前方法所用的类加载器。

我们看下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()加载会使类被初始化,然后就会使上面的静态方法被执行,所以Mysql就将自己的驱动注册到了DriverManager中了,还有一点需要注意,Class.forName()方法加载一个类,使用的类加载器是其调用者的类加载器。

再进行一个小扩充,如果你用自定义类加载器加载了一个Class,那么该类的代码中new这种形式加载的类的类加载器也就是你自定义的类加载器,其实这也就能想到tomcat内部的类加载的一些问题了:

public class Student {	
	public void s(){
		SSS s = new SSS();
		System.out.println(s.getClass().getClassLoader());
	}
}

public class T{
    public static void main(String[] args) {
	    ClassLoader1 loader1 = new ClassLoader1(); 
        // 使用自定义的类加载器去加载一个类
	    Class<?> loadClass = loader1.loadClass("bean.Student");
        // 不能使用强制类型转换,否则会出现类型转换异常		
	    Object newInstance = loadClass.newInstance();
        // 调用该类的s方法,该方法中会去加载另外一个类
        // 我们只能使用这种方式去调用这个方法
	    Method method = loadClass.getMethod("s");
        // 结果会显示出我们自定义的类加载器
	    method.invoke(newInstance);
    }
}

回到Class.forName

    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        // 获取调用者
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

现在我们使用JDBC的方式(对双亲委派机制产生了破坏)

在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就不需要我们手动的去加载驱动了,我们只需要直接获取连接就可以了。

具体的spi的实现过程可以去查询其他文章,我只做简单分析

1.扫描所有的jar包,去找META-INF/services/java.sql.Driver文件中获取具体的实现类名

2.利用Class.forName("")去加载该类

那么问题就来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,是由启动类加载器进行加载的,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/jre/lib下(可以通过System.getProperty("sun.boot.class.path")获取启动类加载器加载的路径),所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

这时候该怎么办?因为存在这样的问题,所以这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。下面看看JDBC中是怎么去应用的呢

该方法是在静态代码块中进行调用,也就是说DriverManager被初始化时就会调用该方法

    private static void loadInitialDrivers() {


        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;
            }
        });
       
    }

删减了根据属性加载的部分,剩下的就是查找各个sql厂商在自己的jar包中记录接口的实现类,spi就会自动的去加载对应的类

然后我们看下ServiceLoader.load()的具体实现:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器去加载驱动类,这里就破坏了双亲委派机制
    // ServiceLoader也是由启动类进行加载的
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

可以看到核心就是拿到线程上下文类加载器,然后构造了一个ServiceLoader,后续的具体查找过程,我们不再深入分析,这里只要知道这个ServiceLoader已经拿到了线程上下文类加载器即可。
接下来,DriverManager的loadInitialDrivers()方法中有一句driversIterator.next(),它的具体实现如下:

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                // cn就是数据库厂商提供的类名类名
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
}

现在,我们成功的做到了通过线程上下文类加载器拿到了应用程序类加载器(或者自定义的然后塞到线程上下文中的),同时我们也查找到了厂商在自己的jar包中声明的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了。

总结

这个时候我们再看下整个mysql的驱动加载过程:

  • 获取线程上下文类加载器,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)
  • 从META-INF/services/java.sql.Driver文件中获取java.sql.Driver接口的实现类名“com.mysql.jdbc.Driver”
  • 通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端

很明显,spi服务确确实实是破坏了双亲委派模型的,毕竟做到了父级类加载器加载了子级路径中的类。


版权声明:本文为P19777原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。