java中ClassLoader

类加载器的基本功能为:

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


一、类加载器的概述

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

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

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

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

[java]  view plain copy
  1. String str = new String('hello world');  
  2. Class<?> clazz1 = str.getClass();  
  3. ClassLoader cl = clazz1.getClassLoader();  
  4. Class<?> clazz2 = cl.loadClass("java.lang.String");//加载类对象,参数为 Java类的二进制名称  
  5. Object o = clazz2.newInstance();  
  6. 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个层次:

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

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

[java]  view plain copy
  1. ClassLoader current = getClass().getClassLoader();  
  2. while(current != null){  
  3.     System.out.print(current.toString());  
  4.     current = current.getParent();  
  5. }  

输出:

[java]  view plain copy
  1. sun.misc.Launcher$AppClassLoader@177b3cd  
  2. 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类的对象。

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

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

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

[java]  view plain copy
  1. public class ParentLastClassLoader extends ClassLoader{  
  2.    protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{  
  3.       Class<?> clazz = findLoadedCLass(name);  
  4.       if(clazz !=null){  
  5.          return clazz;  
  6.       }  
  7.       clazz = findClass(name);  
  8.       if(clazz != null){  
  9.          return clazz;  
  10.       }  
  11.       ClassLoader parent = getParent();  
  12.       if(parent != null){  
  13.          return parent.loadClass(name);  
  14.       }else{  
  15.          return super.loadClass(name, resolve);  
  16.       }  
  17.    }  
  18. }  

六、类加载器的隔离作用

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

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

1、Java类的全名是否相等

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

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


使用场景:

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

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

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

[java]  view plain copy
  1. public interface Versionized{  
  2.    String getVersion();  
  3. }  
[java]  view plain copy
  1. public class ServiceFactory{  
  2.    public static Versionized getService(String className,String version)throws Exception{  
  3.       Path  path = Paths.get("service",version);  
  4.       FileSystemClassLoader loader = new FileSystemClassLoader(path);  
  5.       Class<?>  clazz = loader.loadClass(className);  
  6.       return (Versionized) class.newInstance();  
  7.    }  
  8. }  
[java]  view plain copy
  1. public class ServiceConsumer{  
  2.    public void consume() throws Exception{  
  3.       String serviceName = 'com.foo.bar';  
  4.       Versionized v1 = ServiceFactory.getService(serviceName,"v1");  
  5.       Versionized v2 = ServiceFactory.getService(serviceName,"v2");  
  6.    }  
  7. }  


七、线程上下文类加载器

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,则通过启动类加载来加载。

例子:

[java]  view plain copy
  1. public class LoadResource{  
  2.    public Properties loadConfig() throws IOexception{  
  3.       ClassLoader loader =  this.getClass().getClassLoader();  
  4.       InputStream input = loader.getResourceAsStream("com/foo/bar/config.properties");  
  5.       if(input == null){  
  6.          throw new IOException("找不到配置文件。");  
  7.       }  
  8.       Properties props = new Properties();        
  9.       props.load(input);        
  10.       return props;     
  11.    }  
  12. }     

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值