SPI与线程上下文类加载器

所属文集:ClassLoader串烧


前提

传送门 :理解当前类加载器,主动加载,自动加载是什么!

需求

程序运行过程中要用到的类,通过当前类加载器自动加载,加载不到(不在当前类加载器的类资源管辖范围),如果要使用这个类,必须指定一个能够加载这个类的加载器去加载,而怎么获取这个加载器是个问题。
程序都是在线程中执行,那么从线程的上下文中去拿最合理,所以就诞生了线程上下文类加载器,这个加载器的是非自动加载,即通过forName 或者 loadClass的方式去加载类。

两种场景

1.当高层提供了统一接口,让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
如SPI.下文会从源码验证。

2.当使用本类 托管类加载,然而加载本类的ClassLoader(当前类加载器)未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管执行类加载(指定类加载器)。
如Spring,看tommcat中如何使用spring加载类!解读这种场景的运用


概念

线程的创建者提供了上下文ClassLoader,供加载类和资源时此线程中运行的代码使用。 如果未设置,则默认值为父线程的ClassLoader上下文。 通常将原始线程的上下文ClassLoader设置为用于加载应用程序的类加载器,默认情况下是AppClassLoader;

获取和设置

获取
Thread#getContextClassLoader()
设置
Thread#setContextClassLoader()

经典用法

线程上下文加载器其实是线程的一个私有数据,跟线程绑定的,这个线程做完启动Context组件的事情后,会被回收到线程池,之后被用来做其他事情,为了不影响其他事情,需要恢复之前的线程上下文加载器。

线程上下文类加载器(TCCL)的使用方法

1.获取原TCCL,orign_cl
try{
2.指定一个cl,给TCCL
(ServiceLoader中,使用tccl来加载类)
} finally{
3.将TCCL 还原为orign_cl
}
SPI技术和TCCL

数据库驱动,java官方核心库定了接口,但是没做实现,三方做了实现;单核心库的代码中要使用三方的实现类.
技术实现上来说就是ServiceLoader类是由bootstrap(bs)类加载的,但是bs类加载器,加载不到三方实现(classpath路径下)的类,那方法就执行不下去了。而classpath路径下的类,是由AppClassLoader加载的,可以想办法在此时,获取到AppClassLoader,从代码执行流程来看,其实都是线程在承载逻辑执行,提供了贯穿整个逻辑的上下文,可以方便的在这个上下文中设置和获取cl。
当然线程上下文类加载器可以使用其他的自定义CL
从ServiceLoader源码中,找到如何使用ThreadContext ClassLoader的;
通过DriverManager来跟踪调试代码.

public class SpiDemo {
    public static void main(String[] args) {
            DriverManager.getConnection("");
}

DriverManager

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
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;
            }
        });

        ...
    }

ServiceLoader.load(Driver.class);

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();//获取线程上下文类加载器
        return ServiceLoader.load(service, cl);//传入cl
    }

new ServiceLoader对象,传入目标类类型,和cl

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }

如果未指定cl,则使用系统类加载器
看reload;

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();
    }
构造一个迭代器,传入了cl
public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

回头再看drivermanager中,加载驱动的代码,从ServiceLoader中,获取一个迭代器,并遍历

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }

loadedDrivers.iterato(); 返回的是 java.util.ServiceLoader#iterator,

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

        };
    }

看起构造,hasNext() 和 next()方法内部都调用了lookupIterator的hasNext 和 next方法,那么继续看lookupIterator的着两个方法,

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

关键点在于hasNextService() nextService()这两个方法

先看nextService()方法,代码中,

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
        }

关键点在 Class.forName(cn,false,loader); 最后一个参数loader就是上文中传入的线程上下文类加载器,那么到此处可以明确的知道所谓的SPI 如何使用的线程上下文类加载器进行类加载;进而弄明白为什么线程类加载器,怎么打破双亲委托机制进行了类加载;
简单的总结;SPI的打破双亲委托机制进行类加载,就是指定类加载器,这个类加载通过线程上下文类加载器来承载(赋值和取出)

hasNextService()方法

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 static final String PREFIX = "META-INF/services/";
所以 fullName : META-INF/services/java.sql.Driver∂
configs = loader.getResources(fullName);加载资源后,通过
pending = parse(service, configs.nextElement());解析资源,获取pending的结果
0 = "com.mysql.jdbc.Driver"
1 = "com.mysql.fabric.jdbc.FabricMySQLDriver"

真正理解线程上下文类加载器(多案例分析)

Java SPI详解

高级开发必须理解的Java中SPI机制
Java界最神秘技术ClassLoader,吃透它看这一篇就够了
走出类加载器的迷宫

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

推荐阅读更多精彩内容