首页 > 编程语言 >Java_讨论类加载器的双亲委派机制

Java_讨论类加载器的双亲委派机制

时间:2024-01-21 19:55:57浏览次数:32  
标签:ClassNotFoundException Java name clazz 双亲 java class 加载

双亲委派机制

简而言之,当某个类加载器在接到加载请求时,优先会将任务委托给父类加载器,一直到最顶层的类加载器,如果不能成功加载,才会尝试自己加载
java.lang.ClassLoader

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) {
            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.
                c = findClass(name);
                ..
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

按照类库的设计思路,如果想实现自己的类加载器,我们应该重写findClass(),这样不会破坏双亲委派机制

优点和限制

优点比较明显,java核心类永远由上层类加载器加载(索引指定位置),不会出现代码里自定义了一个java.lang.String类型,把标准库里覆盖的情况出现;限制呢,jdk9之前的限制(jdk9的模块化还没有看懂)我知道的有两点

SPI接口

java提供了服务提供者接口,允许第三方为这些接口提供实现。拿jdbc举例子,jdbc驱动管理类java.sql.DriverManager显然是核心类,而驱动类在哪呢,在用户的classPath下。问题来了,加载DriverManager使用的上层类加载器,只在特定目录寻找class,找不到驱动类,想找到怎么办?驱动类在用户目录,换一个索引用户目录的类加载器就好了,全名是Application ClassLoader
java设计者将类加载器藏得比较隐晦,并没有直接new一个App类加载器,而是增加了和当前线程绑定的类加载器,在jvm.Main()中被赋值,默认就是App类加载器。严格意义上这不算打破,而是绕过了双亲委派机制
DriverManager.loadInitialDrivers() -> ServiceLoader.load(Driver.class)

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

Tomcat类加载器

Tomcat是Servlet容器,Servlet规范中定义每个Web应用程序都应该使用自己的类加载器,以允许容器中的不同部分以及在容器上运行的Web程序可以拥有类相互隔离和公用的功能
在tomcat里不同的web程序都是放在一个目录下的,web1和web2里定义了相同的类,但是实现不一样;同样的,tomcat自己还有一些代码,web程序若包含同名类同样可能冲突;这很容易想到每个web程序需要有自己的类加载器,而且这个类加载器需要优先加载自己本应用目录下的类。当然,JVM的核心类,还是要放在最开始加载
看一下tomcat的web类加载器是怎么玩的,javaseLoader就是JVM的核心类加载器了,思路还是清晰的
org.apache.catalina.loader.WebappClassLoaderBase

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> clazz = null;
        ..

        // (0.2) Try loading the class with the system class loader, to prevent
        //       the webapp from overriding Java SE classes. This implements
        //       SRV.10.7.2
        String resourceName = binaryNameToPath(name, false);

        ClassLoader javaseLoader = getJavaseClassLoader();
        boolean tryLoadingFromJavaseLoader;
        try {
            // Use getResource as it won't trigger an expensive
            // ClassNotFoundException if the resource is not available from
            // the Java SE class loader. However (see
            // https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
            // details) when running under a security manager in rare cases
            // this call may trigger a ClassCircularityError.
            tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null);
        } catch (ClassCircularityError cce) {
            // The getResource() trick won't work for this class. We have to
            // try loading it directly and accept that we might get a
            // ClassNotFoundException.
            tryLoadingFromJavaseLoader = true;
        }

        if (tryLoadingFromJavaseLoader) {
            try {
                clazz = javaseLoader.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }

        boolean delegateLoad = delegate || filter(name, true);

        // (1) Delegate to our parent if requested
        if (delegateLoad) {
            ..
        }

        // (2) Search local repositories
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled())
                    log.debug("  Loading class from local repository");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // (3) Delegate to parent unconditionally
        if (!delegateLoad) {
            if (log.isDebugEnabled())
                log.debug("  Delegating to parent classloader at end: " + parent);
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (log.isDebugEnabled())
                        log.debug("  Loading class from parent");
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }
    }

    throw new ClassNotFoundException(name);
}

测试

第一个SPI不容易理解,本地模拟一下不同类加载器的表现,展示为什么需要打破双亲委托机制

不同类加载器加载的类是不同的

ClassLoader myLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        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 (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
};
Class<?> testClazz = myLoader.loadClass("my.ClassLoaderTest");
System.out.println("the classLoader of Test is " + testClazz.getClassLoader());
Object obj = testClazz.newInstance();
System.out.println(obj instanceof ClassLoaderTest);


the classLoader of Test is my.ClassLoaderTest$1@232204a1
false

不同加载器之间的目录隔阂

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                name = name.replace("@", "");
                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 (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Class<?> testClazz = myLoader.loadClass("my.Test");
        System.out.println("the classLoader of Test is " + testClazz.getClassLoader());
        Object obj = testClazz.newInstance();
        Method test = testClazz.getDeclaredMethod("test");
//        Thread.currentThread().setContextClassLoader(myLoader);
        test.invoke(obj);
    }
}

public class Test {
    public void test() throws ClassNotFoundException {
//        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        System.out.println("the class loader that will be used is " + classLoader);
        Class<?> a = classLoader.loadClass("@my.A");
        System.out.println("the classloader of A is " + a.getClassLoader());
    }
}

class A {}

这里自定义类加载器支持加载@开头的类名,可以理解为同app加载器的一个目录上的区分。这段程序执行是报错的,因为app加载器无法加载@my.A

the classLoader of Test is my.ClassLoaderTest$1@232204a1
the class loader that will be used is sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at my.ClassLoaderTest.main(ClassLoaderTest.java:32)
Caused by: java.lang.ClassNotFoundException: @my.A

如何加载,注释里的内容使用了同SPI一样的解决方式,使用当前线程绑定的类加载器,放开注释

the classLoader of Test is my.ClassLoaderTest$1@232204a1
the class loader that will be used is my.ClassLoaderTest$1@232204a1
the classloader of A is my.ClassLoaderTest$1@232204a1

加载成功。这个例子想表达的是,通过人为添加@,模拟出两种加载器的不同加载目录,通过线程加载器可以解决。想了一下,这似乎只能处理核心类想加载用户类的情况(这对java类库本身已够用),更多的需求,还是要像tomcat一样自定义实现

思考

这里类加载器知识点看了好多年了,之前一直不理解为什么需要打破双亲委派机制(这是可以打破的么??),处于一种似懂非懂的状态里。最近拿起tomcat源码,有一种豁然开朗的感觉,还顺带把之前不理解的SPI也打通了。双亲委派机制只是一种编程规范或者说设计思路,也可以有别的设计想法。知识,总是常读常新。

标签:ClassNotFoundException,Java,name,clazz,双亲,java,class,加载
From: https://www.cnblogs.com/snowsteps/p/17978244

相关文章

  • Java中内部类的使用总结
    ​ 参考文档:Java中内部类的使用总结-CJavaPy1、非静态内部类非静态内部类,也就是成员内部类,是定义在另一个类内部的非静态类。这种内部类与外部类之间有着密切的联系,它可以访问外部类的所有成员(包括私有成员),同时外部类也可以访问内部类的所有成员(包括私有成员)。publicclass......
  • Java中遍历方法对比
    DemopublicclassTest{publicstaticvoidmain(String[]args){test(10);test(100);test(1000);test(10000);}publicstaticvoidtest(intsize){//1.组装数组List<String>list=list(siz......
  • javascript中apply的用法
    javascript中apply的用法欧方2023-03-3118:30江苏在JavaScript中,apply方法和call方法类似,都是用于调用一个函数或方法,不同之处在于apply方法接受一个参数数组作为函数的参数列表。apply方法的语法如下:function.apply(thisArg,[argsArray])其中,t......
  • Java实现Excel导入和导出
    目录目录前言1.功能测试1.1测试准备1.2数据导入1.2.1导入解析为JSON1.2.2导入解析为对象(基础)1.2.3 导入解析为对象(字段自动映射)1.2.4导入解析为对象(获取行号)1.2.5导入解析为对象(获取原始数据)1.2.6 导入解析为对象(获取错误提示)1.2.7导入解析为对象(限制字段......
  • Java连接8.0版本以上的数据库
    一.连接数据库在使用Java连接8.0版本以上的数据库时,可以按照如下步骤:下载需要的包,本次教程中使用的是下面这个版本。该驱动网上有许多资源,可根据自己的需求下载。建立与数据库的连接单元在合适的包下新建"DButil.java"文件并输入如下代码:importjava.sql.Connecti......
  • 在Java中连接8.0版本以上的Mysql数据库
    一.连接数据库在使用Java连接8.0版本以上的数据库时,可以按照如下步骤:下载需要的包,本次教程中使用的是下面这个版本。该驱动网上有许多资源,可根据自己的需求下载。建立与数据库的连接单元在合适的包下新建"DButil.java"文件并输入如下代码:importjava.sql.Connecti......
  • Java魔法值有哪些
    Java魔法值有哪些引言在Java编程中,我们经常会遇到一些被称为魔法值(MagicValue)的常量。这些常量通常以数字的形式出现在代码中,但其含义不太明确,使得代码可读性变差。本文将介绍Java魔法值的概念、常见的魔法值以及如何避免使用魔法值。什么是魔法值?魔法值指的是在代码中直接使......
  • Java每7天日志自动清理
    实现Java每7天日志自动清理作为一名经验丰富的开发者,我很高兴能够教会你如何实现Java每7天日志自动清理的功能。在开始之前,让我们先来了解一下整个流程,然后再逐步进行实现。整体流程如下所示:journeytitleJava每7天日志自动清理流程section步骤清理日志文件-......
  • Java将字符串转为list数组
    将字符串转为list数组的实现方法概述在Java开发中,有时候我们需要将一个字符串转换为一个列表数组,以便对其中的元素进行操作和处理。本文将介绍一种常见的实现方法,并提供详细的步骤和示例代码来帮助你完成这个任务。实现步骤下面是实现将字符串转为list数组的一般步骤,你可以按照......
  • Java将json字符串转换为数组的方法
    Java将json字符串转换为数组的方法在Java开发中,经常会遇到将json字符串转换为数组的需求。JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式,常用于前后端数据传输和存储。而Java中的JSONArray类可以用来处理json数组。下面将介绍一种常用的方法,用于将json字符串转换为......