一 概述
在类加载的阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
《Java虚拟机规范》对于上述三点其实并没有特别具体的要求,留给虚拟机实现与Java应用的灵活度都是相当大的。如“通过一个类的全限定名获取定义此类的二进制字节流”这一规则,它并没有指明二进制字节流必须从某个Class文件中获取,确切地说是根本没有指明从哪里获取,如何获取。从这一点就能够可以创造出各种实现技术,确实有许多举足轻重的Java技术都是建立在这一基础上。
- 从ZIP压缩包中读取,最终成为jar,ear,war格式的基础。
- 从网络中获取,这中场景典型的应用有Web Applet。
- 运行时计算机生成,这种场景使用得很多都是动态代理技术,在java.lang.reflect.Proxy中就是通过了ProxyGenerator.generateProxyClass()来为特定的接口生成形式为"$Proxy"的代理类的二进制字节流。
- 有其他文件生成,典型的场景就是JSP应用,由JSP文件生成对应的Class文件。
- 从数据库中读取,这种场景相对比较少件,如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群中的分发。
- 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障文件运行逻辑不会被窥探。
二 自定义类加载器的实现基础
相对于类加载过程的其他阶段,非数组类型的加载阶段(准确的来说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。如加载阶段既可以使用Java虚拟机里内置的引导类加载来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(如重写一个类加载器的findClass()或loadClass()方法)。
LoadClass(String name,boolean resolve)是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现。resolve参数代表是否生成class对象的同时进行解析相关操作。
重写findClass()
//寻找目标类
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
//JDK默认实现的方法
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的。
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流数组创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
直接使用defineClass会返回一个Class对象,不会对类的Class对象进行解析,使用resolveClass()方法可以使得Class对象被创建完成时也会被解析。
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
//实际由本地方法进行最后的解析工作
private native void resolveClass0(Class<?> c);
三 自定义类加载器的实现代码
自定义File类加载器
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class CustomClassLoader extends ClassLoader {
private String path;
private String classLoaderName;
//Path类文件的路径,classLoaderName类文件的名字
public CustomClassLoader(String path, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name); //将找到类文件以二进制的数组形式加载
return defineClass(name, b, 0, b.length); //JDK默认实现
}
//用于加载类文件,如果文件数据已经加密,可以在方法内进行解密
private byte[] loadFileClassData(String name) {
name = path + name + ".class"; //通过路径和文件名查找文件
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(new File(name));
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) { //!= -1是没有读完的条件
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
自定义网络类加载器:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class CustomClassLoader extends ClassLoader {
private String path;
private String classLoaderName;
//Url类文件的网络路径,classLoaderName类文件的名字
public CustomClassLoader(String Url, String classLoaderName) {
this.path = path;
this.classLoaderName = classLoaderName;
}
//用于寻找类文件
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name); //将找到类文件以二进制的数组形式加载
return defineClass(name, b, 0, b.length); //JDK默认实现
}
//用于加载类文件,如果文件数据已经加密,可以在方法内进行解密
private byte[] loadFileClassData(String name) {
name = url + "/" + className.replace('.', '/') + ".class"; //Url查找文件
InputStream in = null;
ByteArrayOutputStream out = null;
URL url = null;
try {
url = new URL(path);
in = url.openStream();
out = new ByteArrayOutputStream();
int i = 0;
while ((i = in.read()) != -1) { //!= -1是没有读完的条件
out.write(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return out.toByteArray();
}
}
这两种方式的区别主要是在获取字节码流时方法的差异,从网络直接获取到字节流再转车字节数组然后利用defineClass方法创建class对象,如果继承URLClassLoader类则和前面文件路径的实现是类似的,则不需要考虑是filePath还是Url,因为URLClassLoader内的URLClassPath对象会根据传递过来的URL数组中的路径判断是文件还是网络jar包,然后根据不同的路径创建FileLoader或者JarLoader或默认类Loader去读取对于的路径或者url下的class文件。
参考:
- 深入理解Java虚拟机
- ClassLoader.java源码
- URLClassLoader.java与URLClassPath.class的源码