类加载器释疑

Posted 帅性而为1号

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类加载器释疑相关的知识,希望对你有一定的参考价值。

1   由不同的类加载器加载的指定类还是相同的类型吗?

      在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。


2   在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:


//java.lang.Class.java  
publicstatic Class<?> forName(String className) throws ClassNotFoundException   
    return forName0(className, true, ClassLoader.getCallerClassLoader());  
  
  
//java.lang.ClassLoader.java  
// Returns the invoker's class loader, or null if none.  
static ClassLoader getCallerClassLoader()   
    // 获取调用类(caller)的类型  
    Class caller = Reflection.getCallerClass(3);  
    // This can be null if the VM is requesting it  
    if (caller == null)   
        return null;  
      
    // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader  
    return caller.getClassLoader0();  
  
  
//java.lang.Class.java  
//虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法  
native ClassLoader getClassLoader0();

3    在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?

     前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:


    //摘自java.lang.ClassLoader.java  
    protected ClassLoader()   
        SecurityManager security = System.getSecurityManager();  
        if (security != null)   
            security.checkCreateClassLoader();  
          
        this.parent = getSystemClassLoader();  
        initialized = true;  
      


我们再来看一下对应的getSystemClassLoader()方法的实现:


    private static synchronized void initSystemClassLoader()   
        //...  
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
        scl = l.getClassLoader();  
        //...  
      


我们可以写简单的测试代码来测试一下:


System.out.println(sun.misc.Launcher.getLauncher().getClassLoader()); 


本机对应输出如下:


sun.misc.Launcher$AppClassLoader@73d16e93 


所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:
  1. <Java_Runtime_Home>/lib下的类;
  2. < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类;
  3. 当前工程类路径下或者由系统变量java.class.path指定位置中的类。 

4 在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:
  即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。
  说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。


5 编写自定义类加载器时,一般有哪些注意点?

1、一般尽量不要覆写已有的loadClass(...)方法中的委派逻辑
  一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

    //用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)  
    public class WrongClassLoader extends ClassLoader   
      
        public Class<?> loadClass(String name) throws ClassNotFoundException   
            return this.findClass(name);  
          
      
        protected Class<?> findClass(String name) throws ClassNotFoundException   
            // 假设此处只是到工程以外的特定目录D:\\library下去加载类  
            // 具体实现代码省略  
          
      

     通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简单测试一下,现在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程类路径上的类都加载不上了。

    //问题5测试代码一  
    public class WrongClassLoaderTest   
      
        publicstaticvoid main(String[] args)   
            try   
                WrongClassLoader loader = new WrongClassLoader();  
                Class classLoaded = loader.loadClass("beans.Account");  
                System.out.println(classLoaded.getName());  
                System.out.println(classLoaded.getClassLoader());  
             catch (Exception e)   
                e.printStackTrace();  
              
          
      


这里D:"classes"beans"Account.class是物理存在的。输出结果:

java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)  
    at java.io.FileInputStream.open(Native Method)  
    at java.io.FileInputStream.<init>(FileInputStream.java:106)  
    at WrongClassLoader.findClass(WrongClassLoader.java:40)  
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
    at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  
    at java.lang.ClassLoader.defineClass1(Native Method)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
    at WrongClassLoader.findClass(WrongClassLoader.java:43)  
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
    at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  
    at java.lang.ClassLoader.defineClass1(Native Method)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
    at WrongClassLoader.findClass(WrongClassLoader.java:43)  
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
    at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)

这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。
    //问题5测试二  
    //用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)  
    public class WrongClassLoader extends ClassLoader   
      
        protected Class<?> findClass(String name) throws ClassNotFoundException   
            //假设此处只是到工程以外的特定目录D:\\library下去加载类  
            //具体实现代码省略  
          
      

将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:

beans.Account  
WrongClassLoader@1c78e57

       2、正确设置父类加载器
  通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。
   3、保证findClass(String name)方法的逻辑正确性
  事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

6 如何在运行时判断系统类加载器能加载哪些路径下的类?

  一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。
  二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty("java.class.path")。


7 如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

  方法之一:

    import java.net.URL;  
    import java.net.URLClassLoader;  
      
    public class ClassLoaderTest   
      
        /** 
         * @param args the command line arguments 
         */  
        public static void main(String[] args)   
            try   
                URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();  
                for (int i = 0; i < extURLs.length; i++)   
                    System.out.println(extURLs[i]);  
                  
             catch (Exception e)   
                //…  
              
          
      


本机对应输出如下:

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar 


开发自己的类加载器

  在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
  方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。
  类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

  在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。



1 文件系统类加载器

  第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现如下所示。

    package classloader;  
      
    import java.io.ByteArrayOutputStream;  
    import java.io.File;  
    import java.io.FileInputStream;  
    import java.io.IOException;  
    import java.io.InputStream;  
      
    // 文件系统类加载器  
    public class FileSystemClassLoader extends ClassLoader   
      
        private String rootDir;  
      
        public FileSystemClassLoader(String rootDir)   
            this.rootDir = rootDir;  
          
      
        // 获取类的字节码  
        @Override  
        protected Class<?> findClass(String name) throws ClassNotFoundException   
            byte[] classData = getClassData(name);  // 获取类的字节数组  
            if (classData == null)   
                throw new ClassNotFoundException();  
             else   
                return defineClass(name, classData, 0, classData.length);  
              
          
      
        private byte[] getClassData(String className)   
            // 读取类文件的字节  
            String path = classNameToPath(className);  
            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;  
          
      
        private String classNameToPath(String className)   
            // 得到类文件的完全路径  
            return rootDir + File.separatorChar  
                    + className.replace('.', File.separatorChar) + ".class";  
          
      

     如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。 因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
  类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

  加载本地文件系统上的类,示例如下:

    package com.example;  
      
    public class Sample   
      
        private Sample instance;  
      
        public void setSample(Object instance)   
            System.out.println(instance.toString());  
            this.instance = (Sample) instance;  
          
      


    package classloader;  
      
    import java.lang.reflect.Method;  
      
    public class ClassIdentity   
      
        public static void main(String[] args)   
            new ClassIdentity().testClassIdentity();  
          
      
        public void testClassIdentity()   
            String classDataRootPath = "C:\\\\Users\\\\JackZhou\\\\Documents\\\\NetBeansProjects\\\\classloader\\\\build\\\\classes";  
            FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);  
            FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);  
            String className = "com.example.Sample";  
            try   
                Class<?> class1 = fscl1.loadClass(className);  // 加载Sample类  
                Object obj1 = class1.newInstance();  // 创建对象  
                Class<?> class2 = fscl2.loadClass(className);  
                Object obj2 = class2.newInstance();  
                Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);  
                setSampleMethod.invoke(obj1, obj2);  
             catch (Exception e)   
                e.printStackTrace();  
              
          
      

运行输出:com.example.Sample@7852e922


2 网络类加载器


  下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
  类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。

    package classloader;  
      
    import java.io.ByteArrayOutputStream;  
    import java.io.InputStream;  
    import java.net.URL;  
      
    public class NetworkClassLoader extends ClassLoader   
      
        private String rootUrl;  
      
        public NetworkClassLoader(String rootUrl)   
            // 指定URL  
            this.rootUrl = rootUrl;  
          
      
        // 获取类的字节码  
        @Override  
        protected Class<?> findClass(String name) throws ClassNotFoundException   
            byte[] classData = getClassData(name);  
            if (classData == null)   
                throw new ClassNotFoundException();  
             else   
                return defineClass(name, classData, 0, classData.length);  
              
          
      
        private byte[] getClassData(String className)   
            // 从网络上读取的类的字节  
            String path = classNameToPath(className);  
            try   
                URL url = new URL(path);  
                InputStream ins = url.openStream();  
                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 (Exception e)   
                e.printStackTrace();  
              
            return null;  
          
      
        private String classNameToPath(String className)   
            // 得到类文件的URL  
            return rootUrl + "/"  
                    + className.replace('.', '/') + ".class";  
          
      

       在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。 需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:

  客户端接口:

    package classloader;  
      
    public interface Versioned   
      
        String getVersion();  
      

    package classloader;  
      
    public interface ICalculator extends Versioned   
      
        String calculate(String expression);  
      

网络上的不同版本的类:
    package com.example;  
      
    import classloader.ICalculator;  
      
    public class CalculatorBasic implements ICalculator   
      
        @Override  
        public String calculate(String expression)   
            return expression;  
          
      
        @Override  
        public String getVersion()   
            return "1.0";  
          
      
      

    package com.example;  
      
    import classloader.ICalculator;  
      
    public class CalculatorAdvanced implements ICalculator   
      
        @Override  
        public String calculate(String expression)   
            return "Result is " + expression;  
          
      
        @Override  
        public String getVersion()   
            return "2.0";  
          
      
      

在客户端加载网络上的类的过程:
    package classloader;  
      
    public class CalculatorTest   
      
        public static void main(String[] args)   
            String url = "http://localhost:8080/ClassloaderTest/classes";  
            NetworkClassLoader ncl = new NetworkClassLoader(url);  
            String basicClassName = "com.example.CalculatorBasic";  
            String advancedClassName = "com.example.CalculatorAdvanced";  
            try   
                Class<?> clazz = ncl.loadClass(basicClassName);  // 加载一个版本的类  
                ICalculator calculator = (ICalculator) clazz.newInstance();  // 创建对象  
                System.out.println(calculator.getVersion());  
                clazz = ncl.loadClass(advancedClassName);  // 加载另一个版本的类  
                calculator = (ICalculator) clazz.newInstance();  
                System.out.println(calculator.getVersion());  
             catch (Exception e)   
                e.printStackTrace();  
              
          
      
      


参考文献:

http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html

http://www.ibm.com/developerworks/cn/java/j-lo-classloader/





以上是关于类加载器释疑的主要内容,如果未能解决你的问题,请参考以下文章

java之类加载机制

java中类加载器ClassLoader,双亲加载机制,启动类加载器,应用类加载器,线程上下文类加载器

Typescript:使用装饰器时的类型推断

python 判断数据类型及释疑

Spring Boot启动类的run方法

回归泛型类型推断