百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

【JVM类加载】线程上下文加载器分析ServiceLoader.load源码详解

lipiwang 2024-10-27 13:29 9 浏览 0 评论

线程上下文的类加载器(setContextClassLoader)

  • 当前类加载器(Current ClassLoader)
    每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是是所依赖的类),如果classX引用ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未加载)
  • 线程上下文类加载器(Context ClassLoader)
  • 线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器

    线程上下文类加载器的重要性

    • SPI :(Service Provider Interface)服务提供者接口如:jdbc 是用来声明接口制定一些标准(仅仅通过双亲委托来加载类似 父加载器无法看到子类加载器(命名空间) 但是父加载器加载 如 根加载器加载一些rt.jar包里的一些内容如jdbc定义的接口,需要调外部别人实现的一些类时就无法访问到)
    • 父类ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classLoader加载对应的类放到内存中,供父类加载器加载的类使用。这就:改变了父classLoader不能使用子ClassLoader或是其他没有直接父子关系的classLoader加载的类的情况。即改变了双亲委托模型。
    • 线程上下文类加载器就是当前线程的Current Classloader
    • 在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层的加载。但是对于SPI来说,有些接口是java核心库所提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现去来自于不同的jar包(厂商提供)java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。
    • 通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载
    public class MyTest24 {
    
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getContextClassLoader()); //AppClassLoader
            System.out.println(Thread.class.getClassLoader()); //null  Thred位于lang包
        }
    }

    如果没有设置线程上下文类加载器,该线程上下文类加载器默认设置为系统类加载器

    
    public class MyTest25 implements Runnable {
    
        private Thread thread;
    
        public MyTest25() {
            this.thread = new Thread(this);
            thread.start();
        }
    
        @Override
        public void run() {
            ClassLoader classLoader = this.thread.getContextClassLoader();
    
            this.thread.setContextClassLoader(classLoader);
    
            System.out.println("Class: " + classLoader.getClass());
            System.out.println("parent: " + classLoader.getParent().getClass());
        }
    
        public static void main(String[] args) {
            new MyTest25();
        }
    }
    
    /*
        Class: class sun.misc.Launcher$AppClassLoader //如果没有设置线程上下文类加载器 默认设置为 系统类加载器 具体在getLauncher 讲解里
        parent: class sun.misc.Launcher$ExtClassLoader //系统类加载器的父类  只是打破加载规则 并不打破包含规则
    
     */

    线程上下文类加载器的一般使用模式


    线程上下文类加载器的一般使用模式(获取->使用->还原)

    
      //获取
            ClassLoader classLoader = Thread.currentThread.getContextClassLoader();
        try{
            //设置要使用的类加载器
            Thread.currentThread.setContextClassLoader(cls);
            //使用
            MyMethod();
        }finally{
            //如果不还原使用的就是设置的类加载器
            //还原
            Thread.currentThread.setContextClassLoader(classLoader);
        }
    

    上例中:
    MyMethod里面则调用了Thread.currentThread().getContextClassLoader(),获取当前线程的上下文类加载器做某些事情 MyMethod()。

    • 如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话) 如:启动类加载器扫描不到系统类加载器的内容 上下文类加载器就是为了解决这个问题
    • ContextClassLoader的作用就是为了破坏Java的类加载委托机制(如上)
    • 当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)底层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类(上下文类加载器就是为了解决父类加载器加载的类无法看到子类加载器加载的类)我们可以直接使用getClassLoader获取系统类加载器去加载对应的类为什么还要使用上下文类加载器?

    1.方便,执行的任何代码都在线程中 我们可以随时取出来对应的上下文类加载器使用

    2.解决 高层加载底层类问题

    3.有特定的情况,当前的线程类加载器 不一定是系统类加载器 此时不能加载classpath下的.class文件

    import java.sql.Driver;
    import java.util.Iterator;
    import java.util.ServiceLoader;
    
    public class MyTest26 {
    
        public static void main(String[] args) {
            //Driver驱动连接规范的一些信息
            //load()等价与load(Class<S> service, ClassLoader loader)
            //load(需要加载服务的class,Thread.currentThread().getContextClassLoader())
            ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
            Iterator<Driver> iterator = loader.iterator();
    
            while (iterator.hasNext()){
                Driver driver = iterator.next();
                System.out.println("driver: "+driver.getClass()+",loader: "+driver.getClass().getClassLoader());
            }
    
            System.out.println("当前线程上下文类加载器: "+Thread.currentThread().getContextClassLoader());
            System.out.println("ServiceLoader的类加载器:"+loader.getClass().getClassLoader());
        }
    }
    
    /*
    为什么仅仅是Driver.class 这个接口就能找到以下两个Driver实现类
    服务提供者将提供者(供应商)的程序配置文件放在资源目录META-INF/services目录**下当中。
    该文件的名称(如mysql 加载java.sql.Driver文件 类加载器就可以加载这个文件里面每一行的二进制名(类名的路径 此时对应的类就会被加载))
    driver: class com.alibaba.druid.proxy.DruidDriver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2  所有maven下载的jar都位于classpath下
    driver: class com.alibaba.druid.mock.MockDriver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
    driver: class com.mysql.cj.jdbc.Driver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
    ---
    当前线程上下文类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2 未设置默认为系统类加载器
    ServiceLoader的类加载器:null --ServiceLoader位于rt.jar包下
     */
    

    ServiceLoader(服务加载器) 源码分析 (jdk1.6之后)


    1.doc官方文档ServiceLoader(服务加载器)在SPI中的重要作用分析

    ServiceLoader就是用于加载提供者在特定位置META-INF/services 的服务对应服务文件名里的二进制文件名的类

    ServiceLoader是一个服务提供加载者

    • 已知服务的接口(抽象类的集合), 用来加载服务特定的实现(如加载JDBC接口实现类)。提供者(厂商)中的类通常实现服务者接口并继承服务本身中定义的类。服务提供者(如JDBC驱动)可以安装在Java平台的实现中以扩展名的形式,比如说以jar文件被放置到任何常用的扩展目录中。提供程序也可以通过将它们添加到应用程序的类路径(classpath)或其他特定于平台的方法 ,让其变得可用。
    • 对于加载目的,服务由单一类型表示,即单个接口或抽象类。(抽象类可以是已使用,但不建议使用。)给定服务的提供者包含一个或多个具体类,用数据扩展这个服务类型和特定于提供程序的代码。提供程序类是典型的不是整个提供者本身,而是一个包含足够内容的代理决定供应商是否能够满足特定要求的信息请求和代码一起,可以根据需要创建实际的提供者。提供者类的细节往往是高度特定于服务的;没有单个类或接口可以统一它们,所以没有这样的类型在这里定义。这个设施实施的唯一要求是提供者实现类必须有一个无参数的构造函数,这样它们才能在加载期间实例化。
    • 服务提供者(供应商)将提供的的程序配置文件放在驱动包资源目录META-INF/services目录下当中。该文件的名称(如mysql 加载java.sql.Driver文件 类加载器就可以加载这个文件里面每一行的二进制名(类名类名的路径 此时对应的类就会被加载))这是一个服务提供者(sun)制定的标准 是完全限定的 href = " . . / lang /服务类型二进制名。
    • 该文件包含一个完全限定的二进制名的具体提供程序列表,每行放置一个(类的二进制名)。空格和每个字符周围的制表符name和空行将被忽略。注释字符“#”;在每行第一个注释字符后面的所有字符将被忽略。文件名必须用UTF-8编码。(这句话的意思 服务提供者必须要一种方式告诉JDK我的提供者(厂商)具体的类是如何定义在 META-INF/services目录下的,而且文件名是服务类型的名字(给父类使用类二进制名)在这个文件中就可以一行指定一个提供者的类名(实现类) 需要上下文加载器加载的类 #是作为注释)


    • 如果在多个配置文件中出现了同一个具体提供者类的名字的话,或在相同的配置文件中名字出现了一次以上,重复项将被忽略。配置文件特定的提供者不需要在相同的jar文件或其他发行版中单独作为提供者本身。提供者必须是可从相同的类加载器,与最初被定位配置文件相同的类加载器加载出来;注意,这个并不是必要的。
    • 提供程序是按需定位和实例化的。一个服务加载器维护一个已加载的提供者缓存。每次调用{@link #iterator}方法都会返回一个迭代器,它首先返回缓存中的所有元素实例化顺序,然后延迟定位和实例化任何剩余的提供程序,依次将每个提供程序添加到缓存中。可以清除缓存通过{@link #reload reload}方法。(提供者当中的类都是延迟实例化和加载的 换句话说就是什么时候使用就什么时候加载,在serviceLoader中存在缓存,缓存已经加载过的服务提供者提供的类)缓存存储位置

    他是按照实例化顺序添加缓存的

    • 服务加载程序总是在调用者的安全上下文中执行。受信任的系统代码通常应该调用这个类中的方法,并且他们返回的迭代器的方法,从一个特权安全上下文。
    • 并不是线程安全的一个类
    • 除非另有说明,否则将null参数传递给any方法将导致抛出{@link NullPointerException}。
    • 假设我们有一个服务类型com.example.CodecSet (提供者类型与sci规范对应) 这是用于表示某些协议的编码器/解码器对的集合。在这种情况下,它是一个抽象类,有两个抽象方法:

    public abstract Encoder getEncoder(String encodingName);//获取解码器

    public abstract Decoder getDecoder(String encodingName);//获取编码器

    • 每个方法返回一个适当的对象或null(如果提供程序)不支持给定的编码。典型的提供者支持不止一个编码。
    • 如果com.example.impl是com.example.CodecSet(是java定义的一个规范接口的)的具体实现类的话,那么它的jar文件也包含一个名为的 META-INF/services/com.example.CodecSet的文件(对应提供者也必须在指定位置有一个同样的接口名的文件)
    • 这个文件会包含如下这一行com.example.impl.StandardCodecs (类似mysql的驱动
    • CodecSet类创建并保存一个服务实例 如上如果是两个就是加载保存两个实例
    
    private static ServiceLoader<CodecSet>codecSetLoader= ServiceLoader.load(CodecSet.class);
    

    此时load(CodecSet.class); 加载的就是/com.example.CodecSet的文件中对应的二进制名的类的实例
    个人理解就是你要对接java规范中那个服务CodecSet就是对应的服务实例

    • The type of the service to be loaded by this loader (加载的泛型就是要加载的服务的类型

    ServiceLoader.load(String service)源码 解析

    
    public static <S> ServiceLoader<S> load(Class<S> service) {
    		//ServiceLoader在核心库中使用根加载器无法加载对应实现类 
    		//所以的获取当前上下文类加载器(未设置 在launcher中设置为系统类加载器 
    		//基本类加载都会加载Launcher类)
    		//核心代码
            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);
        }
    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();
        }
       public void reload() {
       //清除已加载的提供者缓存
            providers.clear();
            //延迟实例化和加载 (什么时候用什么时候加载) 调用内部类其构造方法
            lookupIterator = new LazyIterator(service, loader);
        }
    
    
     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 {
                      //拼接对应的加载路径  PREFIX 在serviceLoader中定义了"META-INF/services/"; 
                      //SPI规定了 所以加载的就是fullName = 其目录下 +服务接口文件名
                        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 {
                    //使用loader加载对应类名并不实例化 
                    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
            }
    		//下一行 hasNext next remove 用于迭代
            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();
            }
    
        }
    
    

    当设置上下文为扩展类加载器,此时驱动在当前目录下 此时就无法找到对应的jar 此时驱动里的实现类就无法加载

    
    public class MyTest26 {
    
        public static void main(String[] args) {
            //设置上下文为扩展类加载器
            Thread.currentThread().setContextClassLoader(MyTest23.class.getClassLoader().getParent());
    
            //Driver驱动连接规范的一些信息
            //load()等价与load(Class<S> service, ClassLoader loader)
            //load(需要加载服务的class,Thread.currentThread().getContextClassLoader())
            ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
            Iterator<Driver> iterator = loader.iterator();
    
            while (iterator.hasNext()) {
                Driver driver = iterator.next();
                System.out.println("driver: " + driver.getClass() + ",loader: " + driver.getClass().getClassLoader());
            }
    
            System.out.println("当前线程上下文类加载器: " + Thread.currentThread().getContextClassLoader());
            System.out.println("ServiceLoader的类加载器:" + loader.getClass().getClassLoader());
        }
    }

    打印

    当前线程上下文类加载器: sun.misc.Launcher$ExtClassLoader@1eb44e46
    ServiceLoader的类加载器:null

    问题分析P31

    
    public class MyTest27 {
    
        public static void main(String[] args) throws Exception {
            Class.forName("com.mysql.jdbc.Driver");
            DriverManager.getConnection("jdbc:mysql://localhost:3306/mytestdb","username","password");
        }
    }

    注意的点: 此时判断是否是同一个类加载器加载的(命名空间) 避免找不到对应的类

    相关推荐

    《每日电讯报》研发数字工具,教你更有效率地报道新闻

    为鼓励新闻编辑部持续创新,《每日电讯报》正在尝试有战略地研发数字工具。网站的数字媒体主任马尔科姆o科尔斯(MalcolmColes)表示,《每日电讯报》正试图去“创建一些可持续资产”,以便于让记者们...

    html5学得好不好,看掌握多少标签

    html5你了解了多少?如果你还是入门阶段的话,或者还是一知半解的话,那么我们专门为你们收集的html5常用的标签大全对你就很有帮助了,你需要了解了html5有哪些标签你才能够更好的。驾驭html5...

    前端分享-少年了解过iframe么(我想了解少年)

    iframe就像是HTML的「内嵌画布」,允许在页面中加载独立网页,如同在画布上叠加另一幅动态画卷。核心特性包括:独立上下文:每个iframe都拥有独立的DOM/CSS/JS环境(类似浏...

    做SEO要知道什么是AJAX(人能看到但搜索引擎看不到的内容)

    一个明显的,人能看到但搜索引擎不能看到的内容是AJAX。那么什么是AJAX呢?其实,了解过的基本上也都清楚,AJAX不是新的编程语言,而是一种使用现有标准的新方法。AJAX最大的优点是在不重新加...

    介绍最前沿的人工智能创新,‘无反向传播’神经网络训练方法?

    图像由GoogleImageFX生成前言:本文整理自NoProp原始论文与实践代码,并结合多个公开实现细节进行了全流程复现。对神经网络训练机制的探索仍在不断演进,如果你也在研究反向传播之...

    说说我们对HTML6的期许(对html的看法)

    HTML5概述HTML5是HTML语言最受欢迎的版本之一,它支持音频和视频、离线存储、移动端、和标签属性等等。还提供了article,section,header这样的标签来帮助开发者更好...

    浏览器中在线预览pdf文件,pdf.mjs插件实现web预览pdf

    背景:本来只是淘宝上卖卖袜子,想着扩展一下业务,准备做同名“来家居”海外袜子馆外贸项目,碰到pdf在线预览的需求,就找了pdf.js插件进行实践后把此方法记录下来,可以通过多种方法来实现,每种方法都有...

    SVG 在前端的7种使用方法,你还知道哪几种?

    本文简介点赞+关注+收藏=学会了技术一直在演变,在网页中使用SVG的方法也层出不穷。每个时期都有对应的最优解。所以我打算把我知道的7种SVG的使用方法列举出来,有备无患~如果你还...

    HTML5常用标签大全(html5em标签)

    HTML前端开发最终取决于掌握标签的多少HTML大概有七八百个标签楼主这里给大家总结了下HTML常用标签标签描述<!--...-->定义注释。<!DOCTYPE>定义文档类型...

    &quot;伪君子Snoop Dogg!&quot;... WHAT?| MetroDaily 24/7

    TUE.01-新作品-虽说年纪大了会有点糊涂,但是最近SnoopDogg的这波操作实在是让粉丝们有点迷,甚至有人表示没想到他是这样的"伪君子"......而这一切都源于他近日在IG上Po出的一...

    史努比snoopy卡通手机壁纸屏保(史努比壁纸无水印)

    ...

    莎夏·班克斯盼望表哥Snoop Dogg为其作出场曲

    NXT女子冠军莎夏·班克斯(SashaBanks)近日接受了迈阿密先驱报采访,访谈纪要如下:关于她出众的形象:“我一向喜欢与众不同。为了能让人眼前一亮,我的装束总是非常前卫、非常抢眼,这样才能让观众...

    喜欢Snoop!全球第一间「史努比博物馆」海外分馆在东京!

    1950年起,由美國漫畫家CharlesM.Schulz創作的作品《Snoopy》史努比,其鮮明的可愛角色與幽默的劇情內容,至今仍成為許多大朋友與小朋友心中的最愛。為了紀念作者所設立的全球首...

    Vetements 推出 Snoop Dogg 肖像「天价」T-Shirt

    Vetements的CEOGuramGvasalia早前才透露品牌经营策略的秘密–Vetements如何成为人人热议的话题品牌。但似乎他仍有更多需要解释的东西–这个法国奢侈品牌最新...

    狗爷Snoop Dogg的《I Wanna Thank Me》巡回演唱会旧金山站

    西海岸匪帮说唱歌手SnoopDogg在《IWannaThankMe》巡回演唱会旧金山站表演(图片来自ICphoto)西海岸匪帮说唱歌手SnoopDogg(图片来自ICphoto)西海...

    取消回复欢迎 发表评论: