在Java中动态加载class

 

  Java的一个强大的特性是能够动态加载一个给定名称的类,而事先不需要指导这个类的名字。这个特性使得Java的开发人员能够构造一个不需要重新编译即可扩展和修改的灵活动态的系统,在Java中,动态加载通常是调用类java.lang.Class的forName方法来实现;然而,在一个jar包中调用Class.forName会出现一些奇怪的错误。

  下面的内容需要读者具备一定的java知识、ClassLoader知识、编译原理和面向对象的知识。
问题描述:
例如,下面的代码在Main方法中调用ClassLoader来加载一个命令行传入的class.
LoaderTest.java文件
package aa
public class LoaderTest
{
public static void main(String[] args)
{
LoadClass(args[0]);
}

public static void LoadClass(String clsName)
{
try
{
beLoaded bl =
(beLoaded)Class.forName(clsName).newInstance();
bl.PrintInfo();
}
catch (Exception e)
{
e.printStackTrace();
}

}
}

beLoaded.java文件
package aa;
public class beLoaded
{
public void PrintInfo()
{
System.out.println("I am be loaded!");
}
}

  上面的代码在正常情况下非常好使,并且使得整个系统可以同具体的java类分离开来,只需要传入一个class的类名即可完成功能调用。而且从扩展的角度来说,定义一些从beLoaded类上继承下来的类,并将类名通过参数传入系统,即可实现各种不同的功能类的调用,非常方便。
在命令行上键入如下命令:
> java aa.LoaderTest aa.beLoaded
屏幕会输出下面的内容:
I am be loaded!

下面我们创建一个beLoaded的子类,类名叫做beLoadedChild,代码如下:
package aa;

public class beLoadedChild extends beLoaded
{
public void PrintInfo()
{
System.out.println("I am be loaded and I am Child");
}
}
在命令行上键入如下命令:
> java aa.LoaderTest aa.beLoadedChild
屏幕会输出下面的内容:
I am be loaded and I am Child

  通过上面的例子我们可以看出,只要设计好LoaderTest这个类和beLoaded类,就可以实现系统的扩展性,对不同的功能目标调用不同的beLoaded的子类,来完成不同的应用,系统十分的灵活和方便。

  在Java中,为了更好的管理Java的Class,Java允许开发者将一些相关的Class打包放到.jar文件中去,使得系统管理非常的方便,因此我们将上面的aa.LoaderTest和aa.beLoaded这两个类打包到aa.jar中去。将.jar文件放到JAVA_HOME/jre/lib/ext目录中去。
  因为beLoadedChild是子类,所以我们将这个类放在外面,可以随时地修改这个子类而不用重新编译LoaderTest两个类。这里我们将beLoadedClass.class放在当前目录下的aa目录下,具体方法如下:
> java aa.LoaderTest aa.beLoaded
屏幕会输出下面的内容:
I am be loaded!

  上面的beLoaded类在.jar包中存在,所以执行起来没有问题,我们看一看下面的执行情况:
> java aa.LoaderTest aa.beLoadedChild
屏幕会输出下面的内容:
java.lang.ClassNotFoundException: aa.beLoadedChild
at java.net.URLClassLoader$1.run(URLClassLoader.java:199)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:187)
at java.lang.ClassLoader.loadClass(ClassLoader.java:289)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:274)
at java.lang.ClassLoader.loadClass(ClassLoader.java:235)
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:302)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:141)
at aa.LoaderTest.LoadClass(LoaderTest.java:25)
at aa.LoaderTest.main(LoaderTest.java:18)


  系统提示找不到aa.beLoadedChild这个类,可是这是不应该出现的问题,因为beLoadedChild类就在当前目录的aa目录下中,从package的角度来讲是没有问题的,但是问题出在什么地方呢?
问题分析
  分析这个问题,我们可以从两个方面入手,一个是.jar的问题,另一个是java虚拟机的加载机制问题,这两方面最终会统一到java的加载机制问题。
  为什么说是.jar的问题呢?首先从运行情况的比较来看,在所有的类都不在.jar文件中时,程序执行没有问题,在打包以后,执行.jar文件中包含的class没有问题,而.jar文件之外的class就出现了ClassNotFound的错误,通过这个现象我们可以认为问题是由.jar打包而产生的。
  但是反过来,sun公司提供的jdk都是以.jar的形式发放的,他们的.jar包中肯定也会存在动态加载class的问题,比较典型的是jdbc,在使用jdbc时都要使用Class.forName()来加载数据库的驱动,为什么这时没有出现问题呢?
  所以上面的问题表明sun公司的jar包中在动态加载类时肯定进行了特殊的处理或者调用,才使得加载jdbc驱动没有产生ClassNotFound的错误。
  看来问题的根源还是在java虚拟机的加载机制上了。
  为了理解这个问题,你需要理解一些Java ClassLoader模型的基础知识以及JDK1.1同Java2之间的差别。
  Java2 ClassLoader Delegation Model.(Java2 ClassLoader委托模型)
Java2 ClassLoader模型是一种“delegating parent(委托给父ClassLoader)”模型,这意味着当一个ClassLoader被要求加载一个class时,它首先要求它的parent ClassLoader来加载这个类。只有当它的parent 没有加载过这个类并且也不能加载这个类时,它才自己处理这个加载请求。这就是说ClassLoader是一个类似于树的形式的结构,”bootstrap”这个ClassLoader是这棵树的根节点。
  任何一个用户创建的ClassLoader必须拥有一个parent ClassLoader,如果创建时没有提供parent,ClassLoader的创建者假定其parent 是”system”或者ClassLoader。
  在JVM中被加载的每一个Class都会同加载它的ClassLoader保持有隐含的关联,这个关联我们可以通过Class的方法getClassLoader来找到。每一个Class同一个并且只能是一个ClassLoader相关联,并且这个关联不能通过改变ClassLoader的引用来指向另一个ClassLoader。
  通常程序员会直接调用ClassLoder的loadClass方法,JVM通常是隐式的调用该方法。在JDK1.0和1.1模型中,ClassLoader直接覆盖了loadClass方法来负责实现该方法。而在JDK1.2中,loadClass方法调用它的parent的loadClass方法来检查其parent 是否已经加载了class,只有当它的parent加载class失败时,它自己才有机会加载class.
  通过上面的分析我们可以大致的猜测到,直接通过Class.forName来动态加载class,如果该调用在.jar中的话,那么它的加载范围就在整个.jar中,而不是整个java虚拟机的classpath中。根据java的ClassLoader的工作原理我们可以知道,java的ClassLoader是一个继承的过程,整个java虚拟机拥有一个classloader,而其下的每一个.jar文件应该对应一个ClassLoader,这个ClassLoader的加载范围就是整个.jar文件中的范围,所以前面我们做的例子中当加载.jar文件中的class时,就可以正常通过并执行,而加载.jar之外的class时,由于ClassLoader的范围中没有这个class,所以产生ClassNotFound的错误。
  为了证实我们上面所做的猜测,我们来做一个程序试验一下:
  前面说到java的ClassLoader是以一个树的形式存在的,通过这个特点我们可以考虑在加载class时不使用当前类的ClassLoader,而是使用整个应用程序的ClassLoader,也就是树的根节点来加载类,这样肯定就没有问题了;或者我们不用根节点,使用当前类的ClassLoader的parent ClassLoader也许也同样能够解决问题;再或者我们使用当前进程或者线程的ClassLoader也应该能够解决这个问题。

解决方法:
  下面我们来做个试验,将LoaderTest中的方法修改一下,使用当前线程的ClassLoader来加载class,看看是否能够成功,代码如下:
package aa;
public class LoaderTest
{
public static void main(String[] args)
{
LoadClassEx(args[0]);
}
public static void LoadClass(String clsName)
{
try
{
beLoaded bl =
(beLoaded)Class.forName(clsName).newInstance();
bl.PrintInfo();
}
catch (Exception e)
{
e.printStackTrace();
}

}

public static void LoadClassEx(String clsName)
{
try
{
Thread t = Thread.currentThread();
ClassLoader cl = t.getContextClassLoader();
beLoaded bl = (beLoaded)cl.loadClass(clsName).newInstance();
bl.PrintInfo();
}
catch (Exception e)
{
e.printStackTrace();
}

}
}
编译后,打包,并将.jar文件放入到运行目录中去,然后在命令行输入下列命令:
> java aa.LoaderTest aa.beLoaded
屏幕会输出下面的内容:
I am be loaded!

继续输入下列命令:
> java aa.LoaderTest aa.beLoadedChild
屏幕会输出下面的内容:
I am be loaded and I am Child

  这样我们的猜测和运行结果完全一致,在.jar包中的class拥有了加载任意位置的class文件的能力了。

  我们还可以使用ClassLoader cl = ClassLoader.getSystemClassLoader();来获得系统级的ClassLoader,以获得更广泛的加载范围。

你可能感兴趣的:(动态加载)