Java类加载机制:双亲委派机制,还是应该叫做“父委派模型”?

阅读这篇文章,你会了解到:
1.上面是类加载器
2.为什么应该叫做“父委派模型”,而不是“双亲委派机制”
3.在JNDI中,“父委派模型”是怎么被违背的
4.不只是JNDI,还有TOMCAT的类加载器模型是怎样的,他们有无违背“父委派模型”?

一.什么是类加载器

讲“双亲委派机制”前,要先要讲一讲类和类加载器的关系

1.类(Class)

我们在编写代码时,创建的每个“*.java”文件都可以认为是一个类,我们使用“class”去定义一个类,例如String.java。

2.类加载器(Class Loader)

(1)我们定义的类,如果我们要在编码中用到这个类,首先就是要先把“*.java”这个文件编译成class文件,然后由对应的“类加载器”加载到JVM中,我们才能够使用这个“类对象”。
(2)一般的场景下,类的加载是在我们程序启动的时候由jvm来完成,但是有些场景可能需要我们手动去指定加载某个类或找到某个类,这时候就要用到 Class.forName(String className) 加载/找到 这个className对应的类。
(3)如果比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这个两个类就必定不相等。

在我们日常使用中,类加载器默认有下面3种:
(1)Bootstrap Class Loader:
JDK自带的一款类加载器,用于加载JDK内部的类。Bootstrap类加载器用于加载JDK中$JAVA_HOME/jre/lib下面的那些类,比如rt.jar包里面的类。
(2)Extension Class Loader
主要用于加载JDK扩展包里的类。一般$JAVA_HOME/lib/ext下面的包都是通过这个类加载器加载的,这个包下面的类基本上是以javax开头的。
(3)Application Class Loader
用来加载开发人员自己平时写的应用代码的类的,加载存放在classpath路径下的那些应用程序级别的类的。

Java类加载机制:双亲委派机制,还是应该叫做“父委派模型”?_第1张图片

二.为什么应该叫做“父委派模型”,而不是“双亲委派机制”?

这是个很蛋疼的翻译问题,实际上在oracle官方文档上,人家是这样描述的:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.

java平台通过委派模型去加载类。每个类加载器都有一个父加载器。当需要加载类时,会优先委派当前所在的类的加载器的父加载器去加载这个类。如果父加载器无法加载到这个类时,再尝试在当前所在的类的加载器中加载这个类。

所以,java的类加载机制应该叫做“父委派模型”,不应该叫做“双亲委派机制”,“双亲委派机制”这个名字太具有误导性了。

三.“父委派模型”是怎么工作的?

举个例子,当前有个Test.class,需要加载rt.jar中的java.lang.String,那么加载的流程如下图所示,整体的加载流程是向上委托父加载器完成的。

如果整个链路中,父加载器都没有加载这个类,且无法加载这个类时,才会由Test.class所在的加载器去加载某个类(例如希望加载开发人员自定义的类 Test2.class)。

Java类加载机制:双亲委派机制,还是应该叫做“父委派模型”?_第2张图片

四.“父委派模型”有什么好处?

“父委派模型”保证了系统级别的类的安全性,使一些基础类不会受到开发人员“定制化”的破坏。

如果没有使用父委派模型,而是由各个类加载器自行加载的话,如果开发人员自己编写了一个称为java.lang.String的类,并放在程序的ClassPath中,那系统将会出现多个不同的String类, Java类型体系中最基础的行为就无法保证。应用程序也将会变得一片混乱。

五. “父委派模型”什么时候会遭到破坏?

  • 通过预加载的方式;
  • 通过Thread.getContextClassLoader();

1.通过预加载的方式

这里通过一个简单的例子,就拿sql连接来说:

(1)java.sql.DriverManager:rt.jar包中的类,通过Bootstrap加载器加载。
(2)DriverTest:开发人员自定义的实现了java.sql.Driver接口的类型,通过App加载器加载。

开发人员通过DriverManager.registerDriver方法把自己实现的获取连接的Driver实现类加载并注册到DriverManager中。然后DriverManager.getConnection方法会遍历所有注册的Driver,并触发Driver的connect接口来获取连接。(即绕过在DriverManager所在的Bootstrap加载器,因为Bootstrap加载器不能加载开发人员实现的Driver类)

定义一个 DriverTest 类,实现rt.jar里面的java.sql.Driver接口

public class DriverTest implements Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new DriverTest());
            System.out.println("who load DriverTest: " + DriverTest.class.getClassLoader());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        return new Connection() {
        //此处省略一堆代码......
        }
    }
    
	//启动代码
    public static void main(String[] args) {
        try {
			//由AppClassLoader加载DriverTest类
			Class.forName("com.jenson.pratice.classloader.DriverTest");
            System.out.println("who load DriverManager: "+DriverManager.class.getClassLoader());
            //通过rt.jar中的DriverManager去获取链接,DriverManager由BootstrapClassLoader加载
            Connection connection = DriverManager.getConnection("jdbc://");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

此时运行main方法打印:

who load DriverTest: sun.misc.Launcher$AppClassLoader@18b4aac2
who load DriverManager: null
Process finished with exit code 0
---------------------
DriverManager是由Bootstrap加载器的,因而获取不了Bootstrap加载器,所以为null。从父委派模型的机制上看,因为rt.jar是由Bootstrap加载器加载的,所以里面的类,都不能用到rt.jar以外的类。

那么DriverManager.getConnection是怎么调用DriverTest(App加载器)的getConnection方法呢?

因为父委派模型的限制,DriverManager不可能自己去加载DriverTest,DriverTest的加载实际上是由AppClassLoader完成的,DriverTest里面会往
DriverManager中注册一个驱动。

public class DriverTest implements java.sql.Driver {
    static {
        try {
        	//在这里注册
            java.sql.DriverManager.registerDriver(new DriverTest());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

对于DriverManager而言,他不关注driver的加载,他只需要遍历“registeredDrivers”,然后检查驱动类是否能被“调用类的类加载器”识别,如果可以识别,则调用driver.connect方法(即DriverTest中的实现)

 public class DriverManager{
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //省略一堆代码
        for(DriverInfo aDriver : registeredDrivers) {
        //在这里做安全校验
if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    //在这里调用DriverTest的connect方法
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
                //省略一堆代码
                

整体的流程是这样的
Java类加载机制:双亲委派机制,还是应该叫做“父委派模型”?_第3张图片

所以可以看到,在DriverManager中要调用DriverTest的方法,并没有通过“父委派模型”去加载DriverTest,而是由下层的类加载器自行完成类的加载。这里实际上是绕过了“父委派模型”的机制。

2. 通过Thread.getContextClassLoader

Thread类中有一个contextClassLoader属性,称为上下文类加载器。在实例化一个线程时,如果没有设置contextClassLoader属性,默认会从父线程中继承。如果在应用程序的全局范围内都没有设置过多的话,默认为Application Class Loader。

举个例子:rt.jar中的 javax.xml.parsers.FactoryFinder 中的 newInstance方法:
(1)在newInstance中会用到 getProviderClass 方法
(2)在getProviderClass中会用到 SecuritySupport.getContextClassLoader方法
(3)在SecuritySupport.getContextClassLoader中会用到Thread.currentThread().getContextClassLoader()拿到线程上下文类加载器

   /**
     * Create an instance of a class. Delegates to method
     * getProviderClass() in order to load the class.
     * 
     * 定义的JNDI接口
     * @param type Base class / Service interface  of the factory to instantiate
     * JNDI的实现类名
     * @param className Name of the concrete class corresponding to the service provider
     * 加载器:如果为null,则通过线程的上下文加载器进行加载
     * @param cl ClassLoader used to load the factory class. If null
     * current Thread's context classLoader is used to load the factory class.
     * 如果为true,则使用bootstrap加载器。
     * @param useBSClsLoader True if cl=null actually meant bootstrap classLoader. This parameter
     * is needed since DocumentBuilderFactory/SAXParserFactory defined null as context classLoader.
     */
    static <T> T newInstance(Class<T> type, String className, ClassLoader cl,
                             boolean doFallback, boolean useBSClsLoader)
        throws FactoryConfigurationError
    {
        //省略一堆代码
        try {
        	//在这个方法里面,可以通过线程上下文加载器进行加载className对应的类
            Class<?> providerClass = getProviderClass(className, cl, doFallback, useBSClsLoader);
            //省略一堆代码
    static private Class<?> getProviderClass(String className, ClassLoader cl,
            boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
    {
        try {
            if (cl == null) {
                if (useBSClsLoader) {
                    return Class.forName(className, false, FactoryFinder.class.getClassLoader());
                } else {
                    //在这里,会获得线程的上下文加载器去加载类
                    //其中 ss是 SecuritySupport.java
                    cl = ss.getContextClassLoader();
                    if (cl == null) {
                        throw new ClassNotFoundException();
                    }
                    else {
                        return Class.forName(className, false, cl);
                    }
                }
            }
            //省略一堆代码
    }
class SecuritySupport  {
    ClassLoader getContextClassLoader() throws SecurityException{
        return (ClassLoader)
                AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                ClassLoader cl = null;
                //try {
                //获得线程的上下文加载器
                cl = Thread.currentThread().getContextClassLoader();
                //} catch (SecurityException ex) { }

                if (cl == null)
                    cl = ClassLoader.getSystemClassLoader();

                return cl;
            }
        });
    }

六.关于tomcat的类加载机制

不只是Driver驱动的实现是这样,在tomcat、spring等等的容器框架也是通过一些手段去绕过“父委派机制”。

例如下图中的tomat类加载器的结构:
Java类加载机制:双亲委派机制,还是应该叫做“父委派模型”?_第4张图片

从图中的委派关系中可以看出:

  1. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用。
  2. CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
  3. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
  4. JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

那么tomcat 违背了父委派模型吗?

tomcat 违背了父委派模型。因为双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
而tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

七.参考文档

https://www.cnblogs.com/tiancai/p/9317299.html
https://blog.csdn.net/qq_38182963/article/details/78660779
https://www.cnblogs.com/doit8791/p/5820037.html
https://blog.csdn.net/lengxiao1993/article/details/86689331

你可能感兴趣的:(java,tomcat,thread,类加载机制,双亲委派模型,tomcat的类加载器,JNDI的类加载)