类加载器是怎么工作的?
除了辅助加载器(bootstrap class loader)之外,所有的类加载器都有父类加载器(parent class loader).此外,所有的类加载器的类型都是java.lang.ClassLoader. 上述的两点是不同的,并且对于开发者自己写的类加载器来说是非常重要的.
而最重要的一方面是正确地设置父类加载器.所有的类加载器的父类加载器都是加载这个加载器的类加载器的一个实例.(记住一点:一个类加载器也是一个类).
一个类被类加载器请求是通过loadClass()方法来实现的.这个方法的内部实现可以在java.lang.ClassLoader类中找到,下面已给出:
- protected synchronized Class<?> loadClass
- (String name, boolean resolve)
- throws ClassNotFoundException{
- // First check if the class is already loaded
- Class c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClass0(name);
- }
- } catch (ClassNotFoundException e) {
- // If still not found, then invoke
- // findClass to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
为了设置父类加载器,在我们自己的Class loader构造方法中,我们有两个方法来设置:
- public class MyClassLoader extends ClassLoader{
- public MyClassLoader(){
- super(MyClassLoader.class.getClassLoader());
- }
- }
或者:
- public class MyClassLoader extends ClassLoader{
- public MyClassLoader(){
- super(getClass().getClassLoader());
- }
- }
第一个方法比较好,因为当对象的初始化将在构造方法外面完成的时候,在构造方法里调用 getClass()方法是不被提倡的.然而,如果已经正确地设置了父类加载器,无论什么时候一个ClassLoader实例在请求一个class的时候,如果它找不到,它首先应该请求它的父类加载器,如果父类加载器也找不到这个类的话,并且findBootstrapClass0()方法也失败的话,就会调用方法findClass().默认实现的findClass()方法会抛出ClassNotFoundException,如果开发者想要创建自己的ClassLoader,继承java.lang.ClassLoader类时,就可以实现这个方法.默认的findClass()方法实现如下:
- protected Class<?> findClass(String name)
- throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
在findClass方法的内部,类加载器需要从任意的资源中取得字节码.资源可以是 文件系统,一个网络URL,一个数据库,可以转换成字节码其他的应用程序,或者任何有能力生成符合JAVA字节码规范的资源.你甚至可以使用BCEL(byte code engineering libary),它可以在运行时提供方便的方法来创建类.BCEL已经被成功的用在一些项目上,如编译器,优化器,模糊程序(obsfuscators),代码生成器和分析工具.如果一旦重新得到字节码,方法defineClass()将会被调用,并且运行时对于调用这个方法的类加载器是非常特别的.然而,如果两个类加载器实例分别从相同或者不同的数据库源创建字节码,这样创建的类是不同的.
JAVA语言规范对在JAVA执行引擎中加载,链接和初始化类和接口的过程给出了详细的解释.
图1显示了一个主类为MyMainClass的应用程序,如图所示,类MyMainClass将会被AppClassLoader.MyMainClass创建的两个实例(CustoClassLoader1,customClassLoader2)所加载,并且他们可以从一些资源(这里是从一个网络路径)里的一个第四方类Target中找到字节码.这就意味着Target的类定义不是在应用程序或者扩展的CLASSPATH中.这样的情况下,如果MyMainClass类请求客户类加载器来加载Target类的话,Target类会被加载并且类Target.class会被CustomClassLoader1和CustomClassLoader2分别单独的定义.这点在JAVA里影响很大. 如果在Target 类中有些静态初始化的代码,并且我们如果只想这些代码被虚拟机执行且仅且一次,而在我们现在的结构中那些代码将会分别被那些CustomClassLoaders执行一次.但是如果Target类被所有的CustomClassLoader来初始化,就像下面图1中的target1和target2,这时候 target1和target2的类型是不一样的.换句话说,虚拟机不能执行这些代码:
- Target target3 = (Target) target2;
上面的代码将会抛出一个ClassCastException.由于他们是由不同的类加载器定义的,虚拟机会认为这两个实例是截然不同的两种类型.就算MyMainClass类不用两个类型不同的类加载器CustomClassLoader1和CustomCLassLoader2,而是用两个具有相同的CustomClassLoader类型的不同实例来加载的话,上面的结论也是成立的.