类加载器顺序-另一种绕开双亲委派的方式【yetdone 危险的案例】and 加载顺序总结 and 三种绕开方式总结

 

类加载器 classpath 摘要认为:

加载顺序总结

MyClassLoader查看本加载器缓存,native findLoadedClass

MyClassLoader交给父加载器loadclass

MyClassLoader调用本类的findClass

MyClassLoader调用本类的defineClass

 

JDBC注册原理与自定义类加载器解决com.cloudera.hive.jdbc41.HS2Driver的加载【重点】认为:

第一次loadclass MyJdbcProxy

MyClassLoader查看缓存(Class<?> c = findLoadedClass(name);)有无名为MyJdbcProxy的类——没有

MyClassLoader交给父加载器,因为MyJdbcProxy父加载器也没有,因为我们改名了lc3.jars.JdbcProxy---->MyJdbcProxy——也没有

MyClassLoader调用findClass,然后defineClass,native方法将类缓存入MyClassLoader

MyClassLoader第二次loadClass MyJdbcProxy,再次跑到了defineClass,导致异常

 

作者猜测 findClass/defineClass与一开始的findLoadedClass相对应,findClass之后,在本加载器存储了一个缓冲,下一次loadClass之后,在交给父亲之前,会先在自己本地查找,findClass/defineClass作为本地是否有类的缓存的依据

所以在父加载器与子加载器同时有类H时,先子加载器findClass一下,那么此后所有loadClass都会加载自己的

这就像小孩子考试成绩要爸爸签字,先让爸爸试着签一个,然后藏起来,以后考试都可以先拿这个藏起来的签字

 

为了验证这个想法,做了如下设计:

MyMain

  lc4

    FuckPreFindClass MyUrlClassLoader(MySub.url)

    H {new ByW}

    ByW

MySub

  lc4

    H {new ByW}

    ByW

 

FuckPreFindClass中的代码:

package lc4;

import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * Created by joyce on 2020/3/5.
 */
public class FuckPreFindClass {

    public static void main(String []f) throws Exception {
        String dir = "file:/Users/sunyuming/Documents/tool/jars//MySub-1.0.0-jar-with-dependencies.jar";
        URL url = null;
        try {
            url = new URL(dir);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        URL[] urls = {url};
        MyUrlClassLoader myUrlClassLoader = new MyUrlClassLoader(urls);

        /**
         * 重要的一句,H依赖的ByW,必须在这里进行findClass
         */
        myUrlClassLoader.findClass("lc4.ByW");

        // 可在ClassLoader 761行preDefineClass断点

        System.out.println(11);
        Class c1 = myUrlClassLoader.loadClass("lc4.H");
        // AppClassLoader define lc4.H
        doInvoke(c1);
        // AppClassLoader define lc4.ByW

        System.out.println(21);
        // 用findClass取得H;并强行将H缓存进子加载器方法区
        Class c2 = myUrlClassLoader.findClass("lc4.H");
        // MyUrlCLassLoader define lc4.H
        doInvoke(c2);
        // nothing

        System.out.println(31);
        Class c3 = myUrlClassLoader.loadClass("lc4.H");
        doInvoke(c3);

        /**
         * 我本意是在这里加这一句,理想中,以上3个根据双亲委派都应该打印ByW父亲
         * 然而报了重复define异常
         * 所以使用findClass预注册是危险的
         */
    //    myUrlClassLoader.findClass("lc4.ByW");

        System.out.println(41);
        Class c4 = myUrlClassLoader.loadClass("lc4.H");
        doInvoke(c4);
    }

    private static void doInvoke(Class c) throws Exception {
        Object o = c.newInstance();
        Method method = c.getMethod("print");
        method.invoke(o);
    }

    private static class MyUrlClassLoader extends URLClassLoader {

        public MyUrlClassLoader(URL[] urls) {
            super(urls);
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            return super.findClass(name);
        }

    }
}

*依赖的类谁来调?哦,是 c1.getClassLoader().loadClass() —— new操作时调用当前线程的类加载器,还是调用方的类加载器 (三)【重点】

 

输出:

11
H父亲
ByW父亲
21  —— 从这里开始后的findClass及loadClass均返回自定义加载器的类C,然后调用C.class.getClassLoader().loadClass("lc4.ByW")
H儿子
ByW儿子  ——由于自定义加载器一开始就findClass("lc4.ByW")了一下缓存,故都没交给parent就直接返回了自己的类
31
H儿子
ByW儿子
41
H儿子
ByW儿子

 

可以看到,第三次loadClass时,没有去调用父加载器有的那个H,而是先从缓存中拿到第二次findClass的H,这是一个重要的结论

补充:

1 不一定通过native findLoadedClass发现缓存的类,可能是某个我还没发现的方法直接从方法区拿,new操作时调用当前线程的类加载器,还是调用方的类加载器 (三)  中通过调试,new的方式,都没进loadClass,所以情况可能是:

2 加载顺序总结

未知的某个函数查看缓存或方法区,应对new,对于其它loadclass或forname,至下一节点

MyClassLoader查看本加载器缓存,native findLoadedClass

MyClassLoader交给父加载器loadclass

MyClassLoader调用本类的findClass

MyClassLoader调用本类的defineClass

 

3 本例的实践认为,改写并直接调用urlclassloader.findClass是危险不可控的行为,特别是在本例这种有多级依赖的情况,这也与实际情况相近

除非findClass调用链底层的独立类

 

4 三种绕开方式为:

1)使不可见——tomcat、jetty把某个项目的依赖jar没有放在影响范围较大的系统类加载器,而使用一个单独的WebCLassLoader加载,类加载器隔离朴实案例类的相同通过对是否为同一个类加载器进行判断

缺点:有时候很难使之不可见

 

2)把jar包作为资源文件读取,改写loadClass

缺点,较难控制JDBC注册原理与自定义类加载器解决com.cloudera.hive.jdbc41.HS2Driver的加载【重点】使用resource中的jar包资源作为UrlClassloader(二)算是一次尝试,但对于改写findClass(而且这两篇文章只是侧重于读取资源jar包,findClass无法绕开双亲委派), mysql某个版本就不买帐

类加载器顺序-另一种绕开双亲委派的方式(二)通篇改写loadClass 使用改写loadClass对本法做了一个简单实践;      然而,在更复杂的实践  类加载器隔离朴实案例【重点】【yetdone】 中,证明此法仍然是极不稳定,风险极大的

 

3)每次调用loadClass、调用一次findClass预加载,担保以后使用loadClass不会跑到父亲那边找(本文)

——缺点:范围小,内部依赖类不可能各个代码显式findClass,new的类仍然会走loadClass到parent走一圈,本文仅能作为小实验的小技巧

而且很危险而不可控,建议使用第1)2)种方式;若为调用链底层的独立类,可用(new操作时调用当前线程的类加载器,还是调用方的类加载器 (二)  ,new操作时调用当前线程的类加载器,还是调用方的类加载器 (三)这两篇文章就没办法都用了findClass去一个底层独立类)

 

posted on 2020-03-01 16:15  silyvin  阅读(308)  评论(0编辑  收藏  举报