自定义类加载器

1、为什么需要自定义类加载器

  1. 《类加载器》中讲的,默认类加载器只能加载固定路径下的class,如果有特定路径下的class,需要自定义
  2. 安全性:系统自身需要一些jar,class,如果业务类代码中也有相同的class,破坏系统,类似双亲委托安全性

可以看看tomcat自定义类加载器的原因,别的就大同小异了

a)、要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。
b)、由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。
c)、有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided,
表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。
d)、部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,
会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享,
那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。

2、自定义加载器

这儿主要说下我司的自定义类加载器;更复杂点的可以看看tomcat的类加载机制

为什么需要自定义类加载器?这可以参考章节1的答案

主要在于应用与基础平台的隔离,相对应用:可以有更大技术选型自由度,不用考虑基础平台的jar包版本、相对平台:更可靠安全,不被应用class影响

类加载器结构

虽然JAVA使用了类加载的委派机制,但并没严格要求开发者必须遵守该机制,我们可以打破这种"双亲委派"机制

目录结构

目录 说明
/servicesdir 业务实现jar包
/thirddir 业务依赖jar包
/platformdir 平台依赖jar包

类加载器

  • 1.PlatformClassLoader平台加载器
    • 1.1.加载/platformdir下的jar包
    • 1.2.在加载时,采用了默认的“双亲委派”
  • 2.AppClassLoader应用加载器
    • 2.1.加载/servicesdir,/thirddir下的jar
    • 2.2.该类加载器一定程度上打破了默认的“双亲委派”
      • 2.2.0.loadClass方法中,如果本加载器没有load到对应的类,则会检查该类是否处于平台类加载器白名单中:
      • 2.2.1.如果处于白名单中,则委派PlatformClassLoader加载
      • 2.2.2.否则,通过super.loadClass(String,boolean)走默认的双亲委派

此处白名单类:平台核心类,不能被同名业务类干扰

预加载

《类加载器》中说过,程序启动后,并不会加载所有类,在运行中实现到时,才会去加载。这儿就有性能损耗。

按类加载规则,一个类只加载一次

可以测试一下,加载需要的损耗

/**
 * 类加载时间性能测试
 *
 * 看一下类加载需要消耗的时间
 * Created by Jack on 2018/10/8.
 */
public class ClassLoaderTest1 {
    public static void main(String[] args) throws SQLException {
        long s = System.nanoTime();

        LoaderClass loaderClass = new LoaderClass();
        long e = System.nanoTime();
        //第一次时间
        System.out.println(e - s);
        e = System.nanoTime();
        //第二次实例,但已经加载过,不再需要加载
        LoaderClass loaderClass1 = new LoaderClass();
        long e1 = System.nanoTime();
        //第二次时间
        System.out.println(e1 - e);
    }
}
//输出
2409737
396

可以从输出看到性能损耗是不小的,这部分损耗可以通过预加载来消除

随着程序运行时间越久,被触发的业务越多,那加载到的业务类越多。

预加载类的逻辑

ClassWarmUp

  • 1.在classloader中loadClass时,把className加入到LinkedBlockingDeque中
  • 2.为了性能,异步把deque中的class写入到文件中,需要起一个后台线程
    • 2.1 后台线程,从deque中取出class,写入到文件中
  • 3.下次从文件中预先加载class

打包

对于/servicesdir 与 /thirddir 都好处理,但对于platformdir是怎么打包的呢?毕竟在开发时,只是引入一个平台基础jar就行

使用

有了自定义类加载器,在应用主函数中,就不能直接new了,不然就会使用AppClassLoader

所以需要使用反射机制

Class<?> loadClass = platformClassLoader.loadClass("com.jack.Start");
Method startMethod = loadClass.getMethod("startUp");
startMethod.invoke(loadClass);

这样,通过Start加载的类也会通过platformClassLoader去加载

创建springcontext也一样,这儿还需使用到Thread.currentThread().getContextClassLoader()【下面有详解】

ClassLoader currentThreadLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(appClassLoader);

Class<?> contextClass = appClassLoader
                .loadClass("org.springframework.context.support.FileSystemXmlApplicationContext");
Class<?>[] parameterTypes = new Class[] { String[].class };
Constructor<?> constructor = contextClass.getConstructor(parameterTypes);
return constructor.newInstance(new Object[] { xmlPaths.toArray(new String[0]) });


// switch back the thread context classloader
Thread.currentThread().setContextClassLoader(currentThreadLoader);

3、反常

"双亲委派"模型有优点,也有力不从心的地方

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。
而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

场景:

  1. 当高层提供了统一的接口让低层去实现,同时又要在高层加载(或者实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管

解决方案:

从jdk1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader c1),分别用来获取和设置类加载器

一般使用模式:获取-使用-还原

ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 
try{    
    Thread.currentThread().setContextClassLoader(targetTccl);    
    excute(); 
} finally { 
    Thread.currentThread().setContextClassLoader(classLoader); 
}

jdbc

以jdbc看下场景1的情况

Class.forName("com.mysql.jdbc.Driver")
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
  • 1.Class.forName("com.mysql.jdbc.Driver"); 在com.mysql.jdbc.Driver中
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
 
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

通过Class.forName(),主要就是执行初始化static代码块,也就是向DriverManager注册Driver

此时:应用类、Driver是由AppClassLoader加载,但由于双亲委派java.sql.DriverManager是由BootstrapClassLoader加载

  • 2.java.sql.DriverManager.getConnection 获取连接
private static Connection getConnection(
    String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
    java.util.Vector drivers = null;
        
    synchronized(DriverManager.class) {  
      if(callerCL == null) {
          callerCL = Thread.currentThread().getContextClassLoader();
       }    
    } 
     
    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }
    
    println("DriverManager.getConnection(\"" + url + "\")");
    
    if (!initialized) {
        initialize();
    }
 
    synchronized (DriverManager.class){ 
        drivers = readDrivers;  
        }
 
    SQLException reason = null;
    for (int i = 0; i < drivers.size(); i++) {
        DriverInfo di = (DriverInfo)drivers.elementAt(i);
  
        if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
        println("    skipping: " + di);
        continue;
        }
        try {
        println("    trying " + di);
        Connection result = di.driver.connect(url, info);
        if (result != null) {
            // Success!
            println("getConnection returning " + di);
            return (result);
        }
        } catch (SQLException ex) {
        if (reason == null) {
            reason = ex;
        }
        }
    }
 
    if (reason != null)    {
        println("getConnection failed: " + reason);
        throw reason;
    }
    
    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
    }


private static Class getCallerClass(ClassLoader callerClassLoader, 
                    String driverClassName) {
    Class callerC = null;
 
    try {
        callerC = Class.forName(driverClassName, true, callerClassLoader);
    }
    catch (Exception ex) {
        callerC = null;           // being very careful 
    }
 
    return callerC;
    }

这其中有两行代码:

callerCL = Thread.currentThread().getContextClassLoader();

callerC = Class.forName(driverClassName, true, callerClassLoader);

这儿是取线程上下文中的classloader,也就是AppClassLoader;如果不取此classloader,那么Class.forName(driverClassName)就是使用DriverManager的BootstrapClassLoader加载,那必然是加载不到,这也就是父层类加载器加载不了低层类。

还有个问题,为什么在应用程序中已经加载过Driver,到了getConnection()又要再加载,还得通过Thread.currentThread().getContextClassLoader()?

其实在getConnection()中,只是对比class是否是同一个,像tomcat那样,各个应用都有自己的mysql-driver的jar包,就只能通过classloader来区分,因为class是不是相同需要classname+classloader组合鉴别

spring

对于场景2的问题

如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean

org.springframework.web.context.ContextLoader类

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
            throw new IllegalStateException(
                    "Cannot initialize context because there is already a root application context present - " +
                    "check whether you have multiple ContextLoader* definitions in your web.xml!");
        }

        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring root WebApplicationContext");
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }
        long startTime = System.currentTimeMillis();

        try {
            // Determine parent for root web application context, if any.
            ApplicationContext parent = loadParentContext(servletContext);

            // Store context in local instance variable, to guarantee that
            // it is available on ServletContext shutdown.
            this.context = createWebApplicationContext(servletContext, parent);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }

            if (logger.isDebugEnabled()) {
                logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
                        WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
            }
            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
            }

            return this.context;
        }
        catch (RuntimeException ex) {
            logger.error("Context initialization failed", ex);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
            throw ex;
        }
        catch (Error err) {
            logger.error("Context initialization failed", err);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
            throw err;
        }
    }

关键代码:

// 获取线程上下文类加载器,默认为WebAppClassLoader
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
// 如果spring的jar包放在每个webapp自己的目录中
// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
if (ccl == ContextLoader.class.getClassLoader()) {
    currentContext = this.context;
}
else if (ccl != null) {
    // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来
    // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
    currentContextPerThread.put(ccl, this.context);
}

这样做的目的在于当通过ConetxtLoader的静态方法获取context的时候,能保证获取的是当前web application的context.实际上就是对于tomcat下面的任何一个线程,我们都能很方便的找出这个线程对应的webapplicationContext.于是在一些不能方便获取servletContext的场合,我们可以通过当前线程获取webapplicationContext.

public static WebApplicationContext getCurrentWebApplicationContext() {
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        if (ccl != null) {
            WebApplicationContext ccpt = currentContextPerThread.get(ccl);
            if (ccpt != null) {
                return ccpt;
            }
        }
        return currentContext;
    }

总结

简而言之就是ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作

4、参考资料

以jdbc为例搞清contextClassLoader

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

推荐阅读更多精彩内容

  • 开篇点题 纵观所有游戏攻略,其本意无非就一点:看完我的攻略并实践,就能增加胜利的把握哦。 但是,有没有教人家输的攻...
    半山徐风雾自清阅读 737评论 6 3
  • 宽广的海域 寻不到幸福的背影 如果不曾相遇 如果我还藏在海底 会不会没有悲伤的结局 还是祝福你 你会有你的公主你的...
    爱听故事的依米阅读 354评论 1 3
  • Android 自定义控件或多或少都会用到Canvas,那么我们就需要熟悉它的API。Canvas给我们提供了大量...
    joker_fu阅读 2,620评论 0 0
  • 许多天以来,我都在被那个吃得苦,霸得蛮的单亲妈妈感动着,也对存在于她身上的这种现象深深的思考着。 不知道从哪一天起...
    我心喜悦阅读 255评论 1 4
  • 历史上,很多名将,在战场上所向披靡,百战百胜,但搞zz却幼稚得出奇。而“术业有专攻”,给别人打下手,和在战...
    扣舷而歌阅读 1,066评论 4 10