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

双亲委派机制

简而言之,当某个类加载器在接到加载请求时,优先会将任务委托给父类加载器,一直到最顶层的类加载器,如果不能成功加载,才会尝试自己加载
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也打通了。双亲委派机制只是一种编程规范或者说设计思路,也可以有别的设计想法。知识,总是常读常新。

posted @ 2024-01-21 19:51  柠檬水请加冰  阅读(14)  评论(0编辑  收藏  举报