一次尝试绕过ClassLoader双亲委派的实验(转)

转载来源 https://blog.csdn.net/Scythe666/article/details/51956047

一、文章来由

来阿里玩Java也有一个多月了,一直对Java虚拟机比较感兴趣,而ClassLoader是整个class载入过程中很重要的组件。而classloader有个双亲委派模型,师兄说这个模型不能破坏,于是打赌一试。

相信如果问:为什么要双亲委派,可能有人可以侃侃而谈,但是说到为什么要这么分层,为什么要分三层,如何绕过双亲委派模型。。。

这就不是那么容易了,这个时候就需要一些专研了。

二、classloader的作用

这个问题我问了师兄:加载+连接的所有过程,但是深入理解Java虚拟机说的不太一样(所以有待考证)

20160719150427843.png

请原谅我贴图,但下面两张图字字珠玑(p228):

20160719150841661.png
20160719151023158.png

classloader虽然只用于实现类的加载动作,但在Java程序中作用却远远不限于类加载阶段,也就是后面说的可以决定类。

三、为什么要3个classloader

我个人认为有两个原因,当然可能不止。

1、是为了安全

http://stackoverflow.com/questions/28011224/what-is-the-reason-for-having-3-class-loaders-in-java

The reason for having the three basic class loaders (Bootstrap, extension, system) is mostly security.

A key concept is the fact that the JVM will not grant package access (the access that methods and fields have if you didn’t specifically mention private, public or protected) unless the class that asks for this access comes from the same class loader that loaded the class it wishes to access.

So, suppose a user calls his class java.lang.MyClass. Theoretically, it could get package access to all the fields and methods in the java.lang package and change the way they work. The language itself doesn’t prevent this. But the JVM will block this, because all the real java.lang classes were loaded by bootstrap class loader. Not the same loader = no access.

There are other security features built into the class loaders that make it hard to do certain types of hacking.

So why three class loaders? Because they represent three levels of trust. The classes that are most trusted are the core API classes. Next are installed extensions, and then classes that appear in the classpath, which means they are local to your machine.

For a more extended explanation, refer to Bill Venners’s “Inside the Java Virtual Machine”.

拥有三个基本类加载器(Bootstrap、Expand、System)的原因主要是安全性。

一个关键的概念是,除非请求此访问的类来自加载了希望访问的类的同一个类加载器,否则JVM将不授予包访问(如果您没有特别提到私有、公共或受保护的,方法和字段所具有的访问)。

因此,假设用户调用他的类java. Lang.MyC类。理论上,它可以获得对Java.郎包中所有字段和方法的包访问,并更改它们的工作方式。语言本身并不能阻止这一点。但是JVM会阻止这一点,因为所有真正的Java.Langclass都由Bootstrap类加载器加载。不是相同的加载器=没有访问。在类加载器中内置了其他安全特性,这使得难以进行某些类型的黑客攻击。

那么为什么是三类装载机呢?因为它们代表了三个层次的信任。最受信任的类是核心API类。接下来是安装扩展,然后出现在类路径中的类,这意味着它们是本地的。

有关更详细的解释,请参阅Bill Venners的“Java虚拟机内部”。

2、另外,这个帖子没有提到的应该是隔离

java的所有类都是由classloader加载的,不同classloader之间加载的类彼此是不可见的。tomcat加载了log4j,容器里servlet也加载了log4j,servlet是看不见tomcat加载的log4j类的,反之亦然。

在深入理解Java虚拟机,p278,提到:

tomcat为了支持权限目录结构,对目录中的类库进行加载和隔离,tomcat自定义了多个类加载器。

也说明了这点。

四、尝试绕过双亲委派

深入理解Java虚拟机,p231,写到:

双亲委派模型在jdk1.2引入,但它不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载方式。

换句话说,也就是可以不用这个模型的,自己实现类加载就可以,毕竟类加载器的原始作用就是:“通过类的全限定名得到类的二进制码流”

来看看双亲委派模型是如何实现的:

/**
     * Loads the class with the specified <a href="#name">binary name</a>.  The
     * default implementation of this method searches for classes in the
     * following order:
     *
     * <ol>
     *
     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
     *   has already been loaded.  </p></li>
     *
     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
     *   on the parent class loader.  If the parent is <tt>null</tt> the class
     *   loader built-in to the virtual machine is used, instead.  </p></li>
     *
     *   <li><p> Invoke the {@link #findClass(String)} method to find the
     *   class.  </p></li>
     *
     * </ol>
     *
     * <p> If the class was found using the above steps, and the
     * <tt>resolve</tt> flag is true, this method will then invoke the {@link
     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
     *
     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
     * #findClass(String)}, rather than this method.  </p>
     *
     * <p> Unless overridden, this method synchronizes on the result of
     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
     * during the entire class loading process.
     *
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

逻辑很清晰,优先给自己的parent加载器加载,特别注意,这里的父加载器,不是类的继承,因为三个classloader都是内存对象,所以他们只是逻辑上的父子关系。

(1)bootstrap classloader是native实现的

(2)extclassloader 和 APPclassloader 都是 URLclassloader 的子类对象

其实我想做的事情很简单,就是尝试自己写一个完全不依靠双亲委派的classloader,但是因为编码量比较大,所以我只尝试绕过APPclassloader,让自己的加载器继承(再次说明是逻辑上继承)于extclassloader

上面的代码已经显示,如果parent加载器没办法加载,就找子classloader的findclass方法,但是我想破坏这个模型,就必须重写classloader的loadclass方法

20160719153125591.png

上代码:

package classloader;

import java.io.*;

/**
 * Created by hupo.wh on 2016/7/18.
 */
public class OverwriteClassLoader extends ClassLoader {

    private String rootDir = "d:\\";

    public Class<?> loadClass(String name)
            throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {

            // First, check if the class has already been loaded
            Class<?> c = findClass(name);

            return c;
        }
    }

    private byte[] getClassData(String className) {
        //String path = classNameToPath(className);
        String path = "D:\\xiaohua\\WhTest\\target\\classes\\helloworld\\HelloWorld.class";
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        else {
            System.out.println("name == "+name);
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    public OverwriteClassLoader(ClassLoader classLoader) {

        super(classLoader);
    }

    protected final Class<?> whdefineClass(String name, byte[] b, int off, int len)
            throws ClassFormatError
    {
        return defineClass("helloworld.HelloWorld", b, off, len, null);
    }

}

/////Main.java
class Main{

    public static void main(String[] args) throws ClassNotFoundException {

        ClassLoader extcl =new Object(){}.getClass().getEnclosingClass().getClassLoader();
        while(extcl.getParent()!=null){

            extcl=extcl.getParent();
        }
        System.out.println("extcl == "+extcl);
        System.out.println("overwriteclassloader == "+OverwriteClassLoader.class.getClassLoader());

        OverwriteClassLoader cl = new OverwriteClassLoader(extcl);
        Class<?> clazz = cl.loadClass("helloworld.HelloWorld");

        System.out.println(clazz.getClassLoader());
    }
}

然而我的程序止步于一个地方:

[图片上传失败...(image-e32dc2-1538280237365)]

[图片上传失败...(image-353afd-1538280237365)]

详细跟断点进去,发现这个地方本来我要加载,helloworld.HelloWorld类,但是加载java.lang.Object类的时候,一个native方法又调回了我的classloader,进入第三部分的第1小部分

SecurityException: Prohibited package name: java.lang

[图片上传失败...(image-b32e9c-1538280237365)]

  • 这又说明了一个问题,classloader去load这个类的父类,也是找我这个类,但是我这个类的loadclass没有双亲委派,同时安全检查又是用的classloader这个类内置的,所以通不过。

后来发现这段代码实际上是有问题的,因为我把InputStream写死了,下面代码才是正确的

其实这个时候,我自己加载的类已经绕过双亲委派了,因为自己这个类是没有去查父亲的,于是有了下面这个更极端的测试~~

package classloader;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Created by hupo.wh on 2016/7/20.
 */
public class ClassLoaderTest {

    public static void main(String[] args) throws Exception {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) {

                try {

                    InputStream is = null;
                    if(name == "helloworld.HelloWorld") {
                        is = new FileInputStream("D:\\xiaohua\\WhTest\\target\\classes\\helloworld\\HelloWorld.class");
                    }
                    else {

                        is = new FileInputStream("D:\\lang\\Object.class");
                        //return super.loadClass(name);
                    }

                    byte [] b = new byte[is.available()];

                    is.read(b);

                    return defineClass(name,b,0,b.length);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        };

        Class<?> clazz = myLoader.loadClass("helloworld.HelloWorld");
        System.out.println(clazz.getClassLoader());
    }
}

我将rt.jar中的java.lang解压在d盘了,当然还是会报那个错误。。。

[图片上传失败...(image-8deb1c-1538280237365)]

于是这样也会报错:

package java.lang;

/**
 * Created by hupo.wh on 2016/7/20.
 */
public class sayHello {

    public static void main(String[] args) {
        System.out.println("hello");
    }
}

[图片上传失败...(image-79f0ba-1538280237365)]

当然也是有方法的,就是完全自己实现classloader这个类,不继承于任何东西,这样的话,jvm也拿你没办法了。

附上深入理解Java虚拟机正确运行源码(p228)

package classloader;

import java.io.FileInputStream;
import java.io.InputStream;

/**
 * Created by hupo.wh on 2016/7/20.
 */
public class ClassLoaderTest2 {

    public static void main(String[] args) throws Exception {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) {

                try {

                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }

                    byte [] b = new byte[is.available()];

                    is.read(b);

                    return defineClass(name,b,0,b.length);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        };

        Class<?> clazz = myLoader.loadClass("helloworld.HelloWorld");
        System.out.println(clazz.getClassLoader());

        Class<?> clazz1 = myLoader.loadClass("org.omg.CORBA.Any");
        System.out.println(clazz1.getClassLoader());
    }
}


参考资料

[1] 深入理解Java虚拟机

--------------------- 本文来自 This is bill 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/Scythe666/article/details/51956047?utm_source=copy

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