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

线程上下文类加载器打破双亲委派(检测线程上下文注入)

lipiwang 2024-11-15 22:02 18 浏览 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核心包需要启动类加载器加载,依照双亲委派模型启动类加载器无法加载其子类中的类也不能委派给子类,因此引入了线程上下文加载器打破了双亲委派模型,将服务商包中的类信息使用线程上下文中的应用类加载器加载。

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

我们下篇文章见!

相关推荐

前端入门——css 网格轨道详细介绍

上篇前端入门——cssGrid网格基础知识整体大概介绍了cssgrid的基本概念及使用方法,本文将介绍创建网格容器时会发生什么?以及在网格容器上使用行、列属性如何定位元素。在本文中,将介绍:...

Islands Architecture(孤岛架构)在携程新版首页的实践

一、项目背景2022,携程PC版首页终于迎来了首次改版,完成了用户体验与技术栈的全面升级。作为与用户连接的重要入口,旧版PC首页已经陪伴携程走过了22年,承担着重要使命的同时,也遇到了很多问题:维护/...

HTML中script标签中的那些属性

HTML中的<script>标签详解在HTML中,<script>标签用于包含或引用JavaScript代码,是前端开发中不可或缺的一部分。通过合理使用<scrip...

CSS 中各种居中你真的玩明白了么

页面布局中最常见的需求就是元素或者文字居中了,但是根据场景的不同,居中也有简单到复杂各种不同的实现方式,本篇就带大家一起了解下,各种场景下,该如何使用CSS实现居中前言页面布局中最常见的需求就是元...

CSS样式更改——列表、表格和轮廓

上篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,这篇文章分享列表、表格和轮廓,一起来看看吧。1.列表List1).列表的类型<ulstyle='list-...

一文吃透 CSS Flex 布局

原文链接:一文吃透CSSFlex布局教学游戏这里有两个小游戏,可用来练习flex布局。塔防游戏送小青蛙回家Flexbox概述Flexbox布局也叫Flex布局,弹性盒子布局。它决定了...

css实现多行文本的展开收起

背景在我们写需求时可能会遇到类似于这样的多行文本展开与收起的场景:那么,如何通过纯css实现这样的效果呢?实现的难点(1)位于多行文本右下角的展开收起按钮。(2)展开和收起两种状态的切换。(3)文本...

css 垂直居中的几种实现方式

前言设计是带有主观色彩的,同样网页设计中的css一样让人摸不头脑。网上列举的实现方式一大把,或许在这里你都看到过,但既然来到这里我希望这篇能让你看有所收获,毕竟这也是前端面试的基础。实现方式备注:...

WordPress固定链接设置

WordPress设置里的最后一项就是固定链接设置,固定链接设置是决定WordPress文章及静态页面URL的重要步骤,从站点的SEO角度来讲也是。固定链接设置决定网站URL,当页面数少的时候,可以一...

面试发愁!吃透 20 道 CSS 核心题,大厂 Offer 轻松拿

前端小伙伴们,是不是一想到面试里的CSS布局题就发愁?写代码时布局总是对不齐,面试官追问兼容性就卡壳,想跳槽却总被“多列等高”“响应式布局”这些问题难住——别担心!从今天起,咱们每天拆解一...

3种CSS清除浮动的方法

今天这篇文章给大家介绍3种CSS清除浮动的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。首先,这里就不讲为什么我们要清楚浮动,反正不清除浮动事多多。下面我就讲3种常用清除浮动的...

2025 年 CSS 终于要支持强大的自定义函数了?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是CSS自定义属性CSS自...

css3属性(transform)的一个css3动画小应用

闲言碎语不多讲,咱们说说css3的transform属性:先上效果:效果说明:当鼠标移到a标签的时候,从右上角滑出二维码。实现方法:HTML代码如下:需要说明的一点是,a链接的跳转需要用javasc...

CSS基础知识(七)CSS背景

一、CSS背景属性1.背景颜色(background-color)属性值:transparent(透明的)或color(颜色)2.背景图片(background-image)属性值:none(没有)...

CSS 水平居中方式二

<divid="parent"><!--定义子级元素--><divid="child">居中布局</div>...

取消回复欢迎 发表评论: