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

本文探讨了Java中的ClassLoader机制,包括其作用、为何采用三层结构、双亲委派模型及其绕过的尝试。通过代码实例展示了自定义ClassLoader的过程。

一、文章来由

这里写图片描述

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

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

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

二、classloader的作用

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

这里写图片描述

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

这里写图片描述

这里写图片描述

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”.

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虚拟机

<think>嗯,用户问的是如何打破JVM的双亲委派模型。首先,我需要回忆一下双亲委派模型的基本概念。双亲委派模型是Java类加载器的工作机制,类加载器在加载类时会先委托父类加载器去尝试加载,只有父类加载器无法完成加载时,子类加载器才会自己加载。这个机制的好处是保证了Java核心库的类型安全,避免重复加载。 接下来,用户想知道如何打破这个模型。我记得在Java中,类加载器的loadClass方法默认实现了双亲委派,如果要打破,通常需要重写这个方法,改变其加载逻辑。比如,先自己尝试加载类,而不是先委托给父类加载器。这可能涉及到自定义类加载器的编写。 然后,我需要考虑具体的实现方法。比如,在自定义类加载器中覆盖loadClass方法,改变委托顺序。或者使用线程上下文类加载器(TCL),在某些场景下,比如JNDI服务,会使用TCL来加载资源,这样可以绕过双亲委派。还有OSGi框架的例子,它通过网状结构的类加载器来实现模块化,每个Bundle有自己的类加载器,相互之间可以委托,这显然打破了双亲委派模型。 不过,用户可能更关注具体的步骤和代码示例。需要举出实际的例子,比如如何重写findClass和loadClass方法,或者如何利用TCL来加载类。同时,应该提到打破双亲委派的应用场景,比如热部署、模块化加载等,这样用户能理解为什么需要这样做。 另外,需要注意打破双亲委派可能带来的问题,比如类冲突、安全性问题等,需要提醒用户谨慎使用。可能用户是遇到了特定的场景,比如动态加载不同的类版本,或者是开发应用服务器需要隔离不同应用的类,这时候打破双亲委派是必要的。 最后,总结一下实现方式,并强调适用场景和潜在风险。确保回答结构清晰,步骤明确,有代码示例更好,这样用户能更好地理解和应用。</think>关于打破双亲委派模型的问题,我们可以从实现原理、典型场景到具体实现分三个层次来解析: --- ### **一、类加载机制基础** 标准双亲委派流程(JDK9+模块化后有所调整): $$ \begin{cases} 1. \text{检查是否已加载} \rightarrow \text{已加载则直接返回} \\ 2. \text{递归调用父加载器的} \ loadClass() \\ 3. \text{父类无法加载时,调用} \ findClass() \ \text{方法} \end{cases} $$ --- ### **二、打破方式及实现原理** #### **1. 线程上下文类加载器(TCL)** ```java // 典型场景:JDBC驱动加载 ClassLoader original = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(new CustomClassLoader()); // SPI服务加载(如java.sql.DriverManager) ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class); } finally { Thread.currentThread().setContextClassLoader(original); } ``` * **实现原理**:通过反向委派机制,由核心类库(如rt.jar)调用TCL加载实现类 #### **2. 重写loadClass()方法** ```java public class BreakDelegateLoader extends ClassLoader { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { // 优先自行加载指定包下的类 if (name.startsWith("com.myapp.hotswap")) { return findClass(name); } return super.loadClass(name); // 其他类仍走双亲委派 } @Override protected Class<?> findClass(String name) { // 从特定路径加载类字节码 byte[] classData = loadClassData(name); return defineClass(name, classData, 0, classData.length); } } ``` * **关键点**:破坏委派顺序,先尝试自己加载再委派给父类 #### **3. OSGi类加载模型** ``` ┌───────────┐ │ Bundle │ │ ClassLoader◄───┐ └─────┬─────┘ │ │ │ 动态委派 ┌─────▼─────┐ │ │ Bundle │ │ │ ClassLoader├───┘ └───────────┘ ``` * **网状结构**:每个Bundle有独立类加载器,通过Import-Package/Export-Package声明依赖关系 --- ### **三、典型应用场景** | 场景 | 实现方式 | 代表案例 | |---------------------|-------------------------|--------------------------| | 热部署 | 自定义类加载器 | Tomcat Webapp隔离 | | SPI服务加载 | 上下文类加载器 | JDBC驱动加载 | | 模块化动态加载 | 网状委派模型 | OSGi/Equinox框架 | | 版本隔离 | 层级覆盖加载 | Maven依赖冲突解决方案 | --- ### **四、注意事项** 1. **类冲突风险**:当不同加载器加载同名类时,JVM会判定为不同类 $$ \text{Class}_A \neq \text{Class}_B \iff \text{ClassLoader}_A \neq \text{ClassLoader}_B $$ 2. **资源泄漏**:自定义类加载器需注意卸载机制,避免PermGen/Metaspace溢出 3. **安全性**:覆盖核心类时需配置安全策略文件 ```java // 设置安全管理器 System.setSecurityManager(new SecurityManager()); ``` 在实际开发中,建议优先遵循双亲委派模型,仅在确实需要类隔离、热加载等场景时谨慎使用破坏性方案。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值