zoukankan      html  css  js  c++  java
  • 7. 通过JDBC源码来分析线程上下文类加载器以及SPI的使用

    声明:本人写的这些,只是当时学习这些知识的理解,不代表是正确的,只是为了巩固记忆,好记忆不如烂笔头,如果哪里有问题,请指出,不喜勿喷,谢谢。

    1. 什么是全盘负责委托机制

    每个类都有自己的类加载器,那么负责加载当前类的类加载器也会去加载当前类中引用的其他类,前提是引用的类没有被加载过
    例如ClassA中有个变量 ClassB,那么加载ClassA的类加载器会去加载ClassB,如果找不到ClassB,则异常。

    2. 为什么需要有线程上下文类加载器

    jvm推荐我们使用双亲委托机制,主要是保证了相同的类不会被重复加载。但是,在jdk1.2之后,提出了线程上下文类加载器的概念,目的是为了打破双亲委托机制,因为在某些场景下(例如:JNDI,JDBC....)等等SPI场景中,关于什么是SPI(服务发现接口),可以参考之前写的文档资料(https://www.cnblogs.com/duguxiaobiao/p/12183135.html),使用双亲委托机制无法实现,那么为什么无法实现呢?

    2.1 使用JDBC的例子,分析为什么双亲委托机制不能实现要求

    原生的JDBC的使用,获取数据库连接使用的是 Connection conn = DriverManager.getConnection(xx,xx,xx);很明显,Connection是jdk提供的接口,具体的实现是我们的厂商例如mysql 实现,加入到项目中,那么设想一下,DriverManager.getConnection(xx,xx,xx);该方法肯定是使用的mysql的jar包,返回了mysql实现的Connection对象,那么加载DriverManager类是由启动类加载器加载,根据上面的全盘负责委托机制来说,启动类加载器会去加载MySql的jar包,很明显,找不到。所以使用双亲委托机制来说,无法实现该SPI场景的需求。

    2.2 线程上下文类加载器的作用

    双亲委托机制:子加载器对应的命名空间包含了父加载器,所以可以实现子容器访问父容器
    线程上下文类加载器:使用该类加载器,可以实现 父容器访问子容器场景,主要设置好上下文类加载器即可。

    3. 线程上下文类加载器的使用

    3.1 线程上下文类加载器使用API

    1. 获取当前线程的上下文类加载器:Thread.currentThread().getContextClassLoader();
    2. 设置当前线程的上下文类加载器:Thread.currentThread().setContextClassLoader(ClassLoader cl);

    3.2 线程上下文类加载器的特征

    1. 如果没有设置 setContextClassLoader(),那么线程将继承父线程的上下文类加载器,这段可以通过Thread.init()方法中可以看出
    2. Java应用运行时初始上下文类加载器是系统类加载器,可以在源码:Launcher类的构造方法中,在实例化系统类加载器后,将之设置为上下文类加载器。

    3.3 线程上下文类加载器使用的通用写法

    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
    try {
        Thread.currentThread().setContextClassLoader(null);
        //其他执行事件
        doSomthing();
    } finally {
        Thread.currentThread().setContextClassLoader(contextClassLoader);
    }
    

    4. 借助JDBC源码分析上下文类加载器的使用

    4.1 示例代码

    Class.forName("com.mysql.jdbc.Driver");
    Connection connection = DriverManager.getConnection("xxx", "xx", "xx");
    

    4.2 源码分析

    4.2.1 首先解析第一句 Class.forName("com.mysql.jdbc.Driver");

    这里就不介绍了Class.forName()源码了,这行代码表示初始化 Driver类,对Driver类的主动使用,就会导致Driver的静态代码块执行,那么我们进入到Driver类中,看是否有需要初始化调用的静态代码块。

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

    可以看到,存在静态代码块,那么进入到静态代码块中,我们来解析 DriverManager.registerDriver(new Driver());

    4.2.2 DriverManager.registerDriver(new Driver());

    可能有的朋友查看该代码源码时,直接就会进入到DriverManager类中查看registerDriver(),其实跟解析第一行一行,主动调用类的静态方法,会导致累的初始化,执行 DriverManager中的静态代码块。所以我们需要先看下面的源码:

    static {
        //首先会添加初始化加载drivers,这里引入了ServiceLoader
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    //因为我们没有设置 jdbc.drivers属性,所以这里只展示关键代码,对于其他不影响流程的代码有所删减,具体的可以看源码
    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;
        }
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //主要的引入各个厂家的Driver类是的服务是在这里加入的
                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;
            }
        });
    }
    

    如果熟悉针对SPI场景的服务加载方式ServiceLoader使用的,当看到这经典的几行的代码时,就知道具体的加载方式了,如果有对ServiceLoader或者SPI不是很熟悉的,可以先阅读或者百度下相关文档(https://www.cnblogs.com/duguxiaobiao/p/12183135.html),那么通过下面这行代码就可以将mysql依赖加载到内存中了。

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

    那么是怎么在DriverManager对应的类加载器启动类加载器中加载到mysql jar包的呢,下面来分析 ServiceLaoder.load()方法,go

    4.2.3 ServiceLaoder.load()

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
    
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    
    //定义的内部类
    private class LazyIterator implements Iterator<S>
    {
    
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        Iterator<String> pending = null;
        String nextName = null;
    
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
    
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
      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 {
                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
      }
    
        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
    
        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
    
        public void remove() {
            throw new UnsupportedOperationException();
        }
    
    }
    

    查阅源码,我们可以看出当我们执行 ServiceLaoder.load()的时候,首先会获取当前线程的上下文类加载器。而且在构造方法中也可以看到,如果获取的上下文类加载器为空时,也会使用默认的系统类加载器,而默认设置当前线程的上下文类加载器的时候,默认运行时也是系统类加载器作为上下文类加载器,所以先肯定一点,后续加载类的类加载器肯定是 系统类加载器。

    致此,通过 ServiceLoader

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

    4.2.4 driversIterator.hasNext(); driversIterator.next();

    从ServiceLoader.iterator()源码中可以看出, driversIterator.hasNext()其实是调用了 load()时候创建的 LazyIterator.hasNext()方法

    public Iterator<S> iterator() {
        return new Iterator<S>() {
            Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
    
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
    
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        };
    }
    

    LazyIterator.hasNext() 最终调用了 LazyIterator.hasNextService()方法,致此,我们就可以看到ServiceLoader是如何在指定目录下获取到指定类名对应的实现类全类名信息的。有兴趣的可以看看。

    我们的重点是在 LazyIterator.next()方法,该方法最终调用了 LazyIterator.nextService()方法,在该方法中我们可以看到如何将mysql的Driver实现类使用上下文类加载器所加载到内存中。

    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        //在遍历的时候获取的当前item的文件内容
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            //是否初始化参数为false,表示这里只加载类,不初始化
            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 {
            //这里才将类初始化,触发mysql Driver类的静态代码块
            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
    }
    

    上面代码说简单点,就是在 hasNext()中找到待实例化的全类名,而在 next()中实例化该类。

    注意:这里有一个问题 我们在最之前的代码中,使用了Class.forName()将mysql驱动的Driver类加载了,这里又重复了一次,那我们的第一步岂不是多余的操作。

    这个疑问很好,也是正确的,所以在新版本的JDBC处理上,已经使用了SPI方式的ServiceLoader加载方式,不在需要第一步骤的手动加载初始化具体的驱动全类名了。

    既然已经触发了mysql Driver累的初始化,那么跟最开始一样,不再重复,这回DriverManager静态代码块已经执行完毕,可以真正的执行 DriverManager.registerDriver(new Driver());了,其实最终也是调用的 DriverManager.registerDriver()

    4.2.5 DriverManager.registerDriver();

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

    从上述代码上看,其实做的东西很简单,就是判断该Driver如果在缓存中没有就添加到缓存中而已。
    致此,第一行代码解析完毕,后面的具体如何获取数据库连接的,跟当前文章想表达的偏离了,所以不再继续了,over

  • 相关阅读:
    Java JVM启动参数
    使用Navicat连接MySQL8.0版本报1251错误
    安装MySQL和出现的问题解决
    跨域问题:解决跨域的三种方案
    Java8 新特性lambda表达式(一)初始
    搭建docker私有仓库
    crontab定时任务
    CentOS610 php环境安装
    Docker常用命令
    PHP调用python脚本执行时报错
  • 原文地址:https://www.cnblogs.com/duguxiaobiao/p/12198013.html
Copyright © 2011-2022 走看看