jdbc破坏双亲委派模型

原生的JDBC中Driver驱动本身只是一个接口,是Java给所有数据库操作提供了的Driver接口,至于具体的实现,是由不同数据库类型去实现的。例如,MySQL的mysql-connector-java-5.1.47.jar

Driver接口具体实现

package java.sql;

import java.util.logging.Logger;

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接口的实现类,字段registeredDrivers

package java.sql;

public class DriverManager {   

// List of registered JDBC drivers

private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    private static Connection getConnection(

        String url, java.util.Properties info, Class<?> caller) throws SQLException {

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

            }

        }

    }

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

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

Class.forName("com.mysql.cj.jdbc.Driver");

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

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

我们看下mysql对Driver接口的实现

package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {

    public Driver() throws SQLException {}

    static {

        try {

    // 将drive写入 DriverManager::registeredDrivers

            DriverManager.registerDriver(new Driver());

        } catch (SQLException var1) {

            throw new RuntimeException("Can't register driver!");

        }

    }

}


Class.forName()加载会使类被初始化(cinit执行static代码块),所以Mysql就将自己的驱动注册到了DriverManager中了,还有一点需要注意,Class.forName()方法加载一个类,使用的类加载器是其调用者的类加载器。

现在我们使用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新增了静态代码块,也就是说在DriveManager类被初始化时就会调用该方法

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;

    }

    //通过SPI加载驱动类    AccessController.doPrivileged(new PrivilegedAction<Void>() {

    //下面的这个run方法通过线程上下文加载器就会自动加载Driver的实现类       

public Void run() {

      //我们在下面分析一下ServiceLoader源代码           

           ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{

                while(driversIterator.hasNext()) {

                /**

                driversIterator.next()最终就是调用 java.util.ServiceLoader类中的Class.forName(DriverName, false, loader)方法。传给 forName 的 loader不能是BootrapLoader,应当是线程上下文加载器

                */                   

                        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.load()的具体实现:

public static <S> ServiceLoader<S> load(Class<S> service) {

    // 获取线程上下文类加载器去加载驱动类,这里就破坏了双亲委派机制

    // ServiceLoader也是由启动类进行加载的

    ClassLoader cl = Thread.currentThread().getContextClassLoader();

    return ServiceLoader.load(service, cl);

}

可以看到核心就是拿到线程上下文类加载器,然后构造了一个ServiceLoader,后续的具体查找过程,我们不再深入分析,这里只要知道这个ServiceLoader已经拿到了线程上下文类加载器即可。

接下来,ServiceLoader的loadInitialDrivers()方法中有一句driversIterator.next(),它的具体实现如下:


private class LazyIterator        implements Iterator<S>

    {

private boolean hasNextService() {

  if (nextName != null) {

          return true;

      }

      if (configs == null) {

          try {

             String fullName = "META-INF/services/" + service.getName();

              if (loader == null)

                  configs = ClassLoader.getSystemResources(fullName);

              else              /**

              ClassLoader中的getResources()方法是从所有jar包中查找符合条件的。

              只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名

              */                 

               configs = loader.getResources(fullName);

          } catch (IOException x) {

              fail(service, "Error locating configuration files", x);

          }

      }

      while ((pending == null) || !pending.hasNext()) {

          if (!configs.hasMoreElements()) {

              return false;

          }

          pending = parse(service, configs.nextElement());

      }

      nextName = pending.next();

      return true;

  }

  private S nextService() {

        if (!hasNextService())

            throw new NoSuchElementException();

        String cn = nextName;

        nextName = null;

        Class<?> c = null;

        try {

        /**

        这里会从扫描得到所有配置信息中的类通过Class.forName(cn, false, loader);进行加载

        如在mysql-connector-java-5.1.25.jar包下的META-INF下有一个配置文件

        java.sql.Driver中配置由com.mysql.jdbc.Driver

        */            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服务确确实实是破坏了双亲委派模型的,毕竟做到了父级类加载器加载了子级路径中的类。

————————————————

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

原文链接:https://blog.csdn.net/P19777/article/details/100829154

https://www.cnblogs.com/liuligang/p/10519771.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容