Java 类加载器
类加载器的基本功能为:从包含字节代码的字节流中定义出虚拟机中的Class类的对象。得到Class的对象之后,一个Java类就可以在虚拟机中自由使用,包括创建新的对象或调用类中的静态方法。一、类加载器的概述java.lang.ClassLoader类是所有由Java代码创建的类的加载器的父类。其本身是通过Java平台提供的启动类加载器(bootstrap class
类加载器的基本功能为:
从包含字节代码的字节流中定义出虚拟机中的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类的对象所在的包的名称。(就是找文件更方便了)。
更多推荐
所有评论(0)