一、类加载器的概念
在《Java类加载机制》这篇文章里详细介绍了加载的整个过程以及类加载器的特点。这里再简要总结一下。
类加载过程的大部分阶段都是由虚拟机自行完成的。唯一的特例是在加载过程的“加载”阶段,即加载过程的第一个阶段,通过一个类的全限定名来获取定义此类的二进制字节流。实现这个动作的模块被称为“类加载器”,而“类加载器”可以分为两类,一类是虚拟机自带的类加载器,启动类加载器(Bootstrap ClassLoader),另一部分则位于虚拟机外部,全都继承自抽象类java.lang.ClassLoader,包括扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader)。
不同的类加载器有各种的命名空间,这些命名空间的关系如下:
-
同一个命名空间内的类是相互可见的。
子加载器的命名空间包含所有父加载器的命名空间。因此子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
当两个不同命名空间内的类相互不可见时,可以采用Java的反射机制来访问实例的属性和方法。
二、为什么要定义类加载器
1. 加密。为了防止java代码被反编译,可以先将编译后的代码加密,而自定义的类加载器负责把这段加密后的代码还原。这里有个问题,如何保证类加载器不被反编译?
2. 从非标准来源加载类。假如部分字节码并不位于本地,而是在数据库或是网络上,那可以自定义类加载器来加载。
3. 动态创建或修改。多个类的字节码都存放在一个文件中,加载类时根据需求读取一部分字节码进行加载类,或者根据实际情况修改部分字节码再载入内存。
三、如何自定义类加载器
自定义类加载器需要完成以下两个个步骤:
-
继承抽象类ClassLoader
覆盖findClass(String)方法,获取到类的字节码,并调用ClassLoader的definClass把字节码转换为Class对象。
一个简单的自定义类加载器如下:
MyClassLoader.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 MyClassLoader extends ClassLoader {
// 加载类的路径
private String path = "";
private final String fileType = ".class";
public MyClassLoader() {
super();// 让系统加载器成为该类的加载器的父类加载器
}
/**
* 获取Class对象
*/
@Override
public Class<?> findClass(String name) {
byte[] data = loaderClassData(name);
return this.defineClass(name, data, 0, data.length);
}
/**
* 读取class文件作为二进制流放入到byte数组中去
*/
private byte[] loaderClassData(String name) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
name = name.replace(".", "/");
try {
is = new FileInputStream(new File(path + name + fileType));
int c = 0;
while (-1 != (c = is.read())) {
baos.write(c);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
public void setPath(String path) {
this.path = path;
}
}
为了验证自定义类加载器是否能正常工作,编写下面的类:
package classloader;
public class MyClass {
static {
System.out.println("MyClass init!");
}
}
编译成功后,重命名类名(为了防止加载时系统的类加载器能够找到类路径),在D盘下新建文件夹classloader,找到MyClass.class复制到该文件夹内。再编写测试类:
package classloader;
public class Test {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader loader = new MyClassLoader();
loader.setPath("d://");
Class<?> clazz = loader.loadClass("classloader.MyClass");
clazz.newInstance();
}
}
运行,可以看到控制台输出“MyClass init!”。
四、能不能自己写一个类叫java.lang.System/String
这个问题写在这里,主要是因为可能会在面试中遇到。实际过程中基本不需要自己去写一个包名跟类名都和系统类完全一样的类吧。
首先,可以把代码写出来,而且如果不去调用,完全不会有问题。假如自己写了一个java.lang.String,然后在其他类中使用的话,会发现用的还是系统的类。因为根据双亲委派机制,遇到需要加载String时,是调用父加载器进行加载的。而父加载器在系统目录中找到String类以后,就不会再去加载自己写的String了。
其次,在自定义的java.lang.String中添加main函数,运行时,会提示“java.lang.NoSuchMethodError: main " ,说明此时加载的还是系统的String。
再次,用自定义的类加载器加载自定义的java.lang.String,也会出现错误提示。“java.lang.SecurityException: Prohibited package name: java.lang”。
最后,假如不覆盖系统类,只是把类的包名定义成java.lang呢?这也是不行的,报错和上一条一样。
所以,结论是,一般不能加载进内存,假如仍然强行加载会报错。