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

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

nanshan 2024-11-10 10:11 12 浏览 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");
        }
    }

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

    相关推荐

    三种自建KMS激活系统自动激活windows方法

    第一种:在windows服务器上搭建主要针对vol版本(win7、win10、win20xx、win2012等等)平台:我自己搭建的windows虚拟机,windows2016的操作系统软件:...

    重装系统被收98元?避开Windows付费陷阱的实用指南

    重装系统被收98元?避开Windows付费陷阱的实用指南有网友反映,在重装Windows系统后,屏幕突然弹出“激活系统需支付98元服务费”的提示,疑惑自己是不是遭遇了付费陷阱。事实上,微软官方的Wi...

    Windows Server2012远程桌面服务配置和授权激活

    安装:注意:安装完毕之后需手动重启一下计算机配置终端服务管理工具---远程桌面服务---RD授权诊断程序,查看当前服务器有没有授权授权:运行—>gpedit.msc->计算机配置---管理...

    新书速览|Windows Server 2022 系统与网站配置实战

    讲述桌面体验、ServerCore/NanoServer,容器与云系统的配置1本书内容《WindowsServer2022系统与网站配置实战》秉持作者一贯理论兼具实践的写作风格,以新版的Wi...

    Windows激活全攻略:KMS神钥与专业工具的完美结合!

    对于许多Windows用户来说,系统的激活是一个必经的过程。虽然Windows操作系统在未经激活的状态下也可以使用一段时间,但长期来看,未激活的系统会限制某些功能并频繁提示用户激活。以下是两种流行的激...

    微软Win9全新激活技术曝光(微软系统激活有什么用)

    2014-07-0905:46:00作者:徐日俄罗斯Wzor日前披露了更多关于Windows9的最新消息,据悉,Windows9将会在今年秋季亮相,其宣传口号是“想要开始按钮和开始菜单?如你所...

    快速激活Windows 10/11:CMD命令详细教程

    #记录我的2024#激活Windows操作系统是确保系统功能和安全更新正常运行的重要步骤。本文将为您分享如何使用命令提示符(CMD)在Windows10和Windows11上进行激活的详细步骤。...

    Wndows 2019 RDS应用发布部署(rds的安装和应用程序的发布)

    安装前的准备1、需要提供服务器作为应用中心,应用中心的推荐配置如下表所示。规格建议1-10人11-20人21-50人51-100人100+人CPU4核8核16核内存8GB16GB32GB64GB系统盘...

    解决 Windows 系统激活难题(如何解决windows激活问题)

    今天,一位朋友给我说,他手头有三台电脑,均同时弹出系统未激活的提示。他对此毫无头绪,便急忙将电脑上出现的激活提示信息一股脑发给了我。我看到其中一台显示的是“Windows10企业版LTSC尚...

    自建KMS激活服务器(自建kms激活服务器的风险)

    自建KMS激活服务器Win10和office安装后,都需要激活才可以使用,一般可以输入购买的MAK激活码进行在线激活,也可以通过KMS激活,网上也有很多激活工具,但这些工具一般都含有病毒或木马程序,容...

    30秒免费激活windows和office亲测有效!

    “第三方工具有病毒?”“KMS服务器激活总失效?”今天给大家分享一个开源激活工具——MicrosoftActivationScripts(MAS),无需密钥、不装软件,30秒永久激活Window...

    「操作系统」Windows 10 LTSC 2019 企业版C大集成更新版

    Windows10LTSC企业版CHIANNET集成更新优化整合多镜像版,CHIANNET,是USBOS超级PE维护盘工具箱作者,长久以来一直默默的更新着,USBOSPE软件,电脑城装机及...

    一文看懂Windows激活:自查方法+授权类型科普(Win7/Win10通用)

    一、如何判断Windows是否永久激活?无论是Win7还是Win10,均可通过以下方法快速验证:命令提示符法(通用):按下Win+R,输入slmgr.vbs/xpr并按回车键运行即可查看是否...

    部分Windows Server 2019/2022用户反馈无法运行微软Teams应用

    IT之家7月2日消息,科技媒体borncity今天(7月2日)发布博文,报道称在多个WindowsServer版本上,MicrosoftTeams应用近期出现了运行故障。用...

    这种Windows激活方式已有20年...(windows现在激活)

    2006年微软正式发布WindowsVista,随之而来引入了一项新的激活机制「OEM激活」,这项机制在Vista和Win7上最为流行。其实WindowsServer自2008开始至2025版本一...

    取消回复欢迎 发表评论: