类加载器的基本功能为:

从包含字节代码的字节流中定义出虚拟机中的Class类的对象。得到Class的对象之后,一个Java类就可以在虚拟机中自由使用,包括创建新的对象或调用类中的静态方法。


一、类加载器的概述

java.lang.ClassLoader类是所有由Java代码创建的类的加载器的父类。

其本身是通过Java平台提供的启动类加载器(bootstrap class loader)来加载的,由原生代码来实现。

启动类加载器负责加载Java自身的核心类到虚拟机中,当他完成初始化工作后,其他继承ClassLoader类的 类加载器就可以正常工作了。

对于Java类和对象,可以通过getClassLoader方法来获得加载它的类加载器对象:

String str = new String('hello world');
Class<?> clazz1 = str.getClass();
ClassLoader cl = clazz1.getClassLoader();
Class<?> clazz2 = cl.loadClass("java.lang.String");//加载类对象,参数为 Java类的二进制名称
Object o = clazz2.newInstance();
System.out.print(o.getClass()); // 输出 java.lang.String


二、Java平台的类加载器主要分为两类:

1、启动类加载器: 由原生代码实现。

2、用户自定义的类加载器: 继承自ClassLoader类。(分为两类)

a、由Java平台默认提供。

    • 扩展类加载器(extension class loader),用来从特定的路径加载Java平台的扩展库
    • 系统类加载器(system class loader),又称应用类加载器(application class loader),根据应用程序运行时的类路径(CLASSPATH)来加载Java类。
      如果程序中没有使用其他自定义的类加载器,则程序本身的Java类都由系统类加载器负责加载。通过系统类加载器对象的getParent方法可以得到扩展类加载器对象。

b、由程序自己创建。


三、定义和初始类加载器

类加载器的根本作用是从字节代码中定义出表示Java类的Class类的对象。这个定义过程由ClassLoader类中的defineClass方法来实现。

定义类加载器(defining class loader):defineClass方法的最终调用者。

初始类加载器(initiating class loader):使用loadClass方法来加载一个Java类的加载器。

例子: 初始类加载器在loadClass方法中把 实际的类加载工作代理给其他类加载器, 后者最终调用了defineClass方法。


四、类加载器的层次结构与代理模式

1、类加载器 是一个 树状结构。

类加载器对象 都可以 有一个 父类的类加载器对象, 通过ClassLoader类的getParent方法可以获取,

构造方法中也提供指定父类类加载器,如果在创建时不指定父类,则默认父类为系统类加载器。

2、如果没有自定义类加载器,则一个Java程序运行时的类加载器通常有3个层次:

从根节点依次是 启动类加载器、 扩展类加载器 和 系统类加载器。

以下为遍历加载器层次结构:

ClassLoader current = getClass().getClassLoader();
while(current != null){
    System.out.print(current.toString());
    current = current.getParent();
}

输出:

sun.misc.Launcher$AppClassLoader@177b3cd
sun.misc.Launcher$ExtClassLoader@1bd7848

这里可以看出 扩展类加载器 是 系统类加载器 的 父类。另外,如果父类是启动类加载器,在部分虚拟机中,getParent()返回为null, 所以这里并没有遍历出。

3、类加载器在加载Java类时通常使用代理模式

在ClassLoader类的默认实现中,在加载类时,会先交给父类加载,当父类无法找到Java类或资源时,才自己加载,这种代理关系会一直向上传递。(父类优先策略)

使用这种策略的原因是,有些类的加载只有父类加载器才能完成,在程序运行过程中,会不断有新的类加载器对象被添加,对于后添加的类加载器来说,加载类锁需要的一些信息对当前类加载器来说是不可见的,这样就只有交给父类来完成。

程序可以根据需要 采用父类优先策略,或覆盖ClassLoader的loadClass方法 来 实现其他策略, 如子类优先策略,或根据加载Java类的名称采取其他策略。


五、创建类加载器

大部分Java程序在运行时并不需要使用自己的类加载器,依靠Java平台提供的3个类加载器就足够了。

在绝大多数时候,也只有系统类加载器发挥作用。

如果程序对加载类的方式有特殊的要求,就要创建自己的类加载器。

通常有以下两种场景:

1、对Java类的字节代码进行特殊的查找和处理,如Java类的字节代码存放在磁盘特定的位置或远程服务器上,或者字节代码经过加密处理。

2、利用类加载器产生的隔离特性来满足特殊的需求。

创建类加载器只需继承ClassLoader即可,可以覆盖其中的一些方法来时间自定义的类加载逻辑。

defineClass:从字节代码中定义出标示Java类的Class类的对象,涉及Java虚拟机的核心功能,从安全角度出发,该方法为final。(由原生代码实现)

其他一些申明为protected的方法,既是创建自定义加载器的基础:

1、loadClass:先看该类是否已经加载;调用父类loadClass,如果没有父类就由启动类加载器进行加载;如果还是没加载到,调用findClass自己加载。

(如果要改变父类优先,改此方法)

2、findLoadedClass:查找该类是否已经被加载,如果是,就返回该Java类对应的Class类对象。

3、findCLass:当代理策略无法使用父类成功加载类时被调用,这个方法主要用来封装当前类加载器自己的类加载逻辑。

(一般自定义类加载器只要覆盖此方法)

4、resolveClass:链接一个定义好的Class类的对象。

例子:从文件系统加载字节代码的类加载器:

public class FileSystemClassLoader extends ClassLoader{
   private Path path;
   public FileSystemClassLoader(Path path){
      this.path = path;
}
   protected Class<?> findClass(String name) throws ClassNotFoundException{
      try{
         byte []  classData = getClassData(name);
         return defineClass(name, classData, 0 , classData.length);
      }catch(IOException e){
         throw new CLassNotFoundException();
      }
   }
   private byte[]  getClassData(String className){
      Path classFilePath = classNameToPath(className);
      return File.readAllBytes(classFilePath);
   }
   private Path classNameToPath(String className){
      return path.resolve(className.replace('.', File.separatorChar) + ".class";
   }
}
FileSystemClassLoader 类只是简单得读取了字节代码的内容,然后给defineClass处理。这里有很多事可以做, 比如处理字节代码的压缩,用ASM,AspectJ对字节代码做增强,再传递给defineClass。扩展一下,可以变成动态生成代码的类加载器,根据参数,使用ASM等工具在运行时生成代码,然后传递给defineClass。

例子:改父类优先 为 当前子类优先策略:

public class ParentLastClassLoader extends ClassLoader{
   protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{
      Class<?> clazz = findLoadedCLass(name);
      if(clazz !=null){
         return clazz;
      }
      clazz = findClass(name);
      if(clazz != null){
         return clazz;
      }
      ClassLoader parent = getParent();
      if(parent != null){
         return parent.loadClass(name);
      }else{
         return super.loadClass(name, resolve);
      }
   }
}

六、类加载器的隔离作用

类加载器的一个重要特性是为它加载的Java类创建了隔离空间,相当于添加了一个新的名称空间。

首先,Java虚拟机是符合判断两个Java类是否相等的:

1、Java类的全名是否相等

2、定义类加载器对象是否相等(即调用defineClass所在的类)

这样,即使同一个类,只要通过不同定义类加载器加载,然后实例出的对象, 互相转换类型,就会抛出ClassCastException异常。


使用场景:

使同名的Java类可以在虚拟机中共存:

版本更新,用户希望使用新的版本,又希望基于老版本的代码可以继续运行。

新老版本保存在不同的文件目录下。

public interface Versionized{
   String getVersion();
}
public class ServiceFactory{
   public static Versionized getService(String className,String version)throws Exception{
      Path  path = Paths.get("service",version);
      FileSystemClassLoader loader = new FileSystemClassLoader(path);
      Class<?>  clazz = loader.loadClass(className);
      return (Versionized) class.newInstance();
   }
}
public class ServiceConsumer{
   public void consume() throws Exception{
      String serviceName = 'com.foo.bar';
      Versionized v1 = ServiceFactory.getService(serviceName,"v1");
      Versionized v2 = ServiceFactory.getService(serviceName,"v2");
   }
}


七、线程上下文类加载器

java.lang.Thread类的两个方法:getContextClassLoader和setContextClassLoader,简单易懂。

如果没有显式调用过setContextClassLoader,线程的上下文类加载器为 父线程的上下文类加载器。

程序启动时的第一个线程的上下文类加载器默认是 Java平台的系统类加载器对象。

线程上下文类加载器,提供了一种直接的方式在程序的各部分之间共享ClassLoader类的对象。(从某种程度上也绕过了树状模型)

比如A类和B类, 需要确保同一个类加载器来加载,一般流程需要在A和B之间 传递ClassLoader对象,而使用线程上下文类加载器提供了更方便和简洁的做法。


八、Class.forName方法

Class.forName方法的作用是根据Java类的名称得到对应的Class类的对象。

Class.forName方法和ClassLoader类的重要区别是,Class.forName可以初始化Java类,而ClassLoader不行。(初始化意味着,静态变量初始化,静态代码块执行)


九、加载资源

使用类加载器加载的资源,通常与class文件保存在同一个,目录下,或同一个jar包中。

与文件操作API想比, 类加载器加载资源可以使用相对路径, 而文件API需要绝对路径。

ClassLoader类中负责加载资源的方法是 getResource(找单个),getResources(找多个),getResourceAsStream(内部调用了getResource得到URL类对象,再调用对象的opernStream获得InpuStream),

加载资源的策略同样是 父类优先,如果父类为null,通过启动类加载器来查找。

与findClass 相对应的 是 findResource方法, 如果要自定义资源查找机制,覆盖此方法。

当需要使用系统类加载器加载资源时,可以直接使用ClassLoader的静态方法,getSystemResource、getSystemResourceAsStream、getSystemResources。他们先得到系统类加载器,再调用对应方法,如果当前系统类加载器为null,则通过启动类加载来加载。

例子:

public class LoadResource{
   public Properties loadConfig() throws IOexception{
      ClassLoader loader =  this.getClass().getClassLoader();
      InputStream input = loader.getResourceAsStream("com/foo/bar/config.properties");
      if(input == null){
         throw new IOException("找不到配置文件。");
      }
      Properties props = new Properties();      
      props.load(input);      
      return props;   
   }
}   

除了ClassLoader类中的方法来加载资源外,Class类中同样有相关方法来加载资源,名称也相同。

内部其实是Class调用了getClassLoader方法得到 ClassLoader类,然后加载资源。

使用Class类的优势是,会对资源名称进行转换,会自动在资源名称前加上Class类的对象所在的包的名称。(就是找文件更方便了)。



Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐