1. 程式人生 > >一次嘗試繞過ClassLoader雙親委派的實驗

一次嘗試繞過ClassLoader雙親委派的實驗

一、文章來由

這裡寫圖片描述

來阿里玩Java也有一個多月了,一直對Java虛擬機器比較感興趣,而ClassLoader是整個class載入過程中很重要的元件。而classloader有個雙親委派模型,師兄說這個模型不能破壞,於是打賭一試。

相信如果問:為什麼要雙親委派,可能有人可以侃侃而談,但是說到為什麼要這麼分層,為什麼要分三層,如何繞過雙親委派模型。。。

這就不是那麼容易了,這個時候就需要一些專研了。

二、classloader的作用

這個問題我問了師兄:載入+連線的所有過程,但是深入理解Java虛擬機器說的不太一樣(所以有待考證)

這裡寫圖片描述

請原諒我貼圖,但下面兩張圖字字珠璣(p228):

這裡寫圖片描述

這裡寫圖片描述

classloader雖然只用於實現類的載入動作,但在Java程式中作用卻遠遠不限於類載入階段,也就是後面說的可以決定類。

三、為什麼要3個classloader

我個人認為有兩個原因,當然可能不止。

1、是為了安全

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.

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方法

這裡寫圖片描述

上程式碼:

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());
    }
}

然而我的程式止步於一個地方:

這裡寫圖片描述

這裡寫圖片描述

詳細跟斷點進去,發現這個地方本來我要載入,helloworld.HelloWorld類,但是載入java.lang.Object類的時候,一個native方法又調回了我的classloader,進入第三部分的第1小部分

SecurityException: Prohibited package name: java.lang

這裡寫圖片描述

  • 這又說明了一個問題,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盤了,當然還是會報那個錯誤。。。

這裡寫圖片描述

於是這樣也會報錯:

package java.lang;

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

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

這裡寫圖片描述

當然也是有方法的,就是完全自己實現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虛擬機器