jvm之线程上下文加载器与SPI

线程上下文加载器

线程上下文类加载器(Thread Context Class Loader,简称TCCL)是从JDK1.2开始引入的。类java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。

如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器,默认为系统类加载器,这点可以从下面的JDK中的源码中得到验证。

以下代码摘自sun.misc.Launch的无参构造函数Launch():

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
...

SPI

Service Provider Interface:服务提供者接口,简称SPI,是Java提供的一套用来被第三方实现或者扩展的API。常见的SPI有JDBC、JNDI、JAXP等,这些SPI的接口由Java核心库实现,而这些SPI的具体实现由第三方jar包实现。

下面先看一段经典的JDBC获取连接的代码:

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

我们可以Class.forName这一行被注释掉了,但依然可以正常运行,这是为什么呢?

下面通过跟踪源码一步一步分析原因:

先看DriverManager这个类,调用该类的静态方法getConnection(),就会导致该类的初始化,也就是会执行这个类的静态代码块,DriverManager的静态代码块如下:

    // 静态代码块
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    // 加载驱动
    private static void loadInitialDrivers() {
        ...

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

                // 读取 META-INF/services
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        // next()的时候才会去加载
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
        ... 
    }

再来看一下ServiceLoader.load方法:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

从load方法中可以看出取出了线程上下文加载器也就是系统类加载器传递给了后面的代码。

一路跟踪下去会发现系统类加载这个参数传给了里面的一个内部类LazyIterator:

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

LazyIterator实现了Iterator接口,后面loadInitialDrivers中获取这个迭代器进行遍历,最终会调用下面的两个方法

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    // META-INF/services/ + java.sql.Driver
                    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;
                }
                // 解析META-INF/services/java.sql.Driver文件
                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 {
                // 使用线程上下文加载器加载META-INF/services/java.sql.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
        }

再来看一下mysql-connector-java.jar包下META-INF/services/java.sql.Driver中内容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

最后看一下com.mysql.jdbc.Driver的静态代码块:

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

registerDriver方法将driver实例注册到JDk的java.sql.DriverManager类中,其实就是add到它的一个类型为CopyOnWriteArrayList,名为registeredDrivers的静态属性中,到此驱动注册基本完成,

总结:如果我们使用JDBC时没有主动使用Class.forName加载mysql的驱动时,那么JDBC会使用SPI机制去查找所有的jar下面的META-INF/services/java.sql.Driver文件,使用Class.forName反射加载其中指定的驱动类。

DriverManager类和ServiceLoader类都是属于rt.jar的,它们的类加载器是根类加载器。而具体的数据库驱动,却属于业务代码,这个根类加载器是无法加载的。而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

使用TCCL校验实例的归属

下面再来看一下java.sql.DriverManager.getConnection()这个方法,这里面有个小细节:

    //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {

        // callerCL是调用这个方法的类所对应的类加载器
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
        ...
        // 遍历注册到registeredDrivers里的Driver类
        for(DriverInfo aDriver : registeredDrivers) {
            // 使用线程上下文类加载器检查Driver类有效性,重点在isDriverAllowed中,方法内容在后面
            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());
            }

        }
        ...
    }

    private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                // 传入的classLoader为调用getConnetction的线程上下文类加载器,使用这个类加载器再次加载驱动类
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }
            // 这里只有aClass和driver.getClass()是由同一个类加载器加载才会相等
             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }

isDriverAllowed这个方法的意义:例如在tomcat中,多个webapp都有自己的Classloader,如果它们都自带mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器加载的Driver实例,webapp想要从DriverManager中获得连接,只有通过线程上下文加载器区分了。

更多精彩内容关注本人公众号:架构师升级之路
在这里插入图片描述

你可能感兴趣的:(jvm,java,jvm,jdk,spi,线程上下文加载器)