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

线程上下文类加载器打破双亲委派 线程上下文是什么

liebian365 2024-11-09 13:48 26 浏览 0 评论

我们在《JVM类加载器》一文中学习了4种类加载器:启动类加载器、扩展类加载器、应用类加载器、用户自定义类加载器。这4种类加载器对类的加载采用了双亲委派模型,大部分类的加载都遵循了双亲委派,但是有的场景下我们需要打破双亲委派。

今天来和勾勾一起学习什么场景下需要打破双亲委派,如何打破双亲委派。


为什么打破双亲委派


JDK的核心库提供了许多的SPI(Service Provider Interface),比如常见的SPI包:JDBC、JCE、JNDI、JAXP和JBI等,JDK只规定了这些接口之间的逻辑关系,但不提供具体的实现,具体的实现是由第三方厂商来提供的。

SPI(service provider interface)是java提供的一种服务发现机制。是一种扩展机制。它可以通过将接口的实现类的全限定类路径写入规定好的文件中,来指定接口使用那个实现类。

比如下图所示,Java中的数据库连接JDBC这个SPI不管数据库切换为什么,应用程序只需要替换JDBC的驱动jar包以及数据库的驱动名称即可,而不用更新核心类库。

我们知道类的加载采用双亲委派模式防止了类的重复加载,而且避免了恶意或者无意对核心API库的破坏。java.lang.sql中的所有接口都是由顶层父类加载器启动类加载器加载的,但是第三方厂商的类库驱动则是由系统类加载器加载的,这种情况按照双亲委派机制启动类加载器是无法加载到JDBC驱动包中的实现类的,这个时候我们就需要打破双亲委派。


类的加载方式


类的加载方式在JDBC源码中使用比较多,我们花费一小会的时间了解一下类的两种加载方式:

  • 隐式加载:new关键字
  • 显式加载:ClassLoader.loadClass和Class.forName

new关键字和Class.forName都是使用当前类加载器,只能在当前类路径下或者导入的类路径下寻找,而ClassLoader可以在当前类路径外寻找类。

Class.forName对应的方法实际是Class.forName(name, true, this.getClass.getClassLoader),true表示加载之后立即进行初始化。

ClassLoader.loadClass对应的方法是ClassLoader.loadClass(className,false),false表示加载之后不需要连接,即此方法加载之后不会进行初始化。

如果类加载完成后需要立即初始化则可以使用Class.forName。


JDBC中的SPI机制


勾勾开发中用的比较多的是MySQL,我们就以MySQL为例来分析JDBC。打开MySQL的Driver源码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            //静态代码块中,注册类驱动到DriverManager中
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

java.sql.DriverManager类存在于JDK中rt.jar包中,采用的类加载器为启动类加载器。在类中有一个静态代码块加载了JDBC驱动器。

 //加载JDBC驱动器
static {  
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}


loadInitialDrivers()中的关键信息是run()方法中代码逻辑。

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;
        }
    //SPI机制加载驱动类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                //创建ServiceLoader对象
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                //获取ServiceLoader的迭代器
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    //循环迭代器
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }


我们针对代码中关键的方法进行分析。

ServiceLoader.load(Driver.class)方法的作用:

1)获取线程的上下文类加载器。

线程上下文类加载器是JDK1.2 引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。

Java 应用运行的初始线程的上下文类加载器是系统类加载器。

//sun.misc.Launcher,启动类加载器通过此方法创建类加载器
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);
    .....
}

2)初始化ServiceLoader,并且初始化ServiceLoader内部类LazyIterator

public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取当前线程的上下文类加载器,此处为应用类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

//new ServiceLoader<>(service, loader)
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    //Class<Driver>类信息不允许为空
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //线程上下加载器如果为空,则获取默认类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    //初始化ServiceLoader内部类LazyIterator
    reload();
}


迭代器初始化之后接下来就需要遍历迭代器。迭代器的主要方法是hasNext()和next()。

hasNext()在迭代器循环的时候主要调用的是hasNextService(),在这个方法中扫描了META-INF/services/路径下的资源文件。

1)service.getName()获取到Driver类的全限定名。根据这个名字就能找到资源文件。

2)parse(service, configs.nextElement())读取资源文件中的内容。


private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // private static final String PREFIX = "META-INF/services/";
            //service.getName()即为Driver的全限定名
            //那么得到的fullName即是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;
        }
        //parse方法用于解析读到的文件的内容
        pending = parse(service, configs.nextElement());
    }
    //nextName得到的即为供应商驱动器的名称
    nextName = pending.next();
    return true;
}


next()调用了nextService()方法将解析到的驱动存入map中。至此便实现了第三方服务的类加载,打破了双亲委派。

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    //nextName即为资源文件中读取的信息
    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());
        //存放到 private LinkedHashMap<String,S> providers = new LinkedHashMap<>();中
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}


总结


SPI机制即是JDK提供了接口规范,第三方服务商只需要遵循规范提供实现即可,因第三方服务商的包需要使用应用类加载器加载,而JDBC核心包需要启动类加载器加载,依照双亲委派模型启动类加载器无法加载其子类中的类也不能委派给子类,因此引入了线程上下文加载器打破了双亲委派模型,将服务商包中的类信息使用线程上下文中的应用类加载器加载。

大家好,我是勾勾,一直在努力的程序媛,感谢您的点赞、关注和转发!

我们下篇文章见!

相关推荐

“版本末期”了?下周平衡补丁!国服最强5套牌!上分首选

明天,酒馆战棋就将迎来大更新,也聊了很多天战棋相关的内容了,趁此机会,给兄弟们穿插一篇构筑模式的卡组推荐!老规矩,我们先来看10职业胜率。目前10职业胜率排名与一周前基本类似,没有太多的变化。平衡补丁...

VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符&quot;

首先,程序中头文件的选择,要选择头文件,在文件中是没有对M_PI的定义的。选择:项目——>”XXX属性"——>配置属性——>C/C++——>预处理器——>预处理器定义,...

东营交警实名曝光一批酒驾人员名单 88人受处罚

齐鲁网·闪电新闻5月24日讯酒后驾驶是对自己和他人生命安全极不负责的行为,为守护大家的平安出行路,东营交警一直将酒驾作为重点打击对象。5月23日,东营交警公布最新一批饮酒、醉酒名单。对以下驾驶人醉酒...

Qt界面——搭配QCustomPlot(qt platform)

这是我第一个使用QCustomPlot控件的上位机,通过串口精确的5ms发送一次数据,再将读取的数据绘制到图表中。界面方面,尝试卡片式设计,外加QSS简单的配了个色。QCustomPlot官网:Qt...

大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写

老友相聚,仗剑江湖!《大话西游2》2021全民PK季4月激燃打响,各PK玩法鏖战齐开,零门槛参与热情高涨。PK季期间,不仅各种玩法奖励丰厚,参与PK趣闻录活动,投稿自己在PK季遇到的趣事,还有机会带走...

测试谷歌VS Code AI 编程插件 Gemini Code Assist

用ClaudeSonnet3.7的天气测试编码,让谷歌VSCodeAI编程插件GeminiCodeAssist自动编程。生成的文件在浏览器中的效果如下:(附源代码)VSCode...

顾爷想知道第4.5期 国服便利性到底需优化啥?

前段时间DNF国服推出了名为“阿拉德B计划”的系列改版计划,截至目前我们已经看到了两项实装。不过关于便利性上,国服似乎还有很多路要走。自从顾爷回归DNF以来,几乎每天都在跟我抱怨关于DNF里面各种各样...

掌握Visual Studio项目配置【基础篇】

1.前言VisualStudio是Windows上最常用的C++集成开发环境之一,简称VS。VS功能十分强大,对应的,其配置系统较为复杂。不管是对于初学者还是有一定开发经验的开发者来说,捋清楚VS...

还嫌LED驱动设计套路深?那就来看看这篇文章吧

随着LED在各个领域的不同应用需求,LED驱动电路也在不断进步和发展。本文从LED的特性入手,推导出适合LED的电源驱动类型,再进一步介绍各类LED驱动设计。设计必读:LED四个关键特性特性一:非线...

Visual Studio Community 2022(VS2022)安装图文方法

直接上步骤:1,首先可以下载安装一个VisualStudio安装器,叫做VisualStudioinstaller。这个安装文件很小,很快就安装完成了。2,打开VisualStudioins...

Qt添加MSVC构建套件的方法(qt添加c++11)

前言有些时候,在Windows下因为某些需求需要使用MSVC编译器对程序进行编译,假设我们安装Qt的时候又只是安装了MingW构建套件,那么此时我们该如何给现有的Qt添加一个MSVC构建套件呢?本文以...

Qt为什么站稳c++GUI的top1(qt c)

为什么现在QT越来越成为c++界面编程的第一选择,从事QT编程多年,在这之前做C++界面都是基于MFC。当时为什么会从MFC转到QT?主要原因是MFC开发界面想做得好看一些十分困难,引用第三方基于MF...

qt开发IDE应该选择VS还是qt creator

如果一个公司选择了qt来开发自己的产品,在面临IDE的选择时会出现vs或者qtcreator,选择qt的IDE需要结合产品需求、部署平台、项目定位、程序猿本身和公司战略,因为大的软件产品需要明确IDE...

Qt 5.14.2超详细安装教程,不会来打我

Qt简介Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开库,主要用来开发图形用户界面(GraphicalUserInterface,GUI)程序。Qt是纯C++开...

Cygwin配置与使用(四)——VI字体和颜色的配置

简介:VI的操作模式,基本上VI可以分为三种状态,分别是命令模式(commandmode)、插入模式(Insertmode)和底行模式(lastlinemode),各模式的功能区分如下:1)...

取消回复欢迎 发表评论: