上节,我们探讨了动态代理,在前几节中,我们多次提到了类加载器 ClassLoader,本节就来详细讨论 Java 中的类加载机制与 ClassLoader。
类加载器 ClassLoader 就是加载其他类的类,它负责将字节码文件加载到内存,创建 Class 对象。与之前介绍的反射、注解、和动态代理一样,在大部分的应用编程中,我们不太需要自己实现 ClassLoader。
不过,理解类加载的机制和过程,有助于我们更好的理解之前介绍的内容,更好的理解 Java。在反射一节,我们介绍过 Class 的静态方法 Class.forName,理解类加载器有助于我们更好的理解该方法。
ClassLoader 一般是系统提供的,不需要自己实现,不过,通过创建自定义的 ClassLoader,可以实现一些强大灵活的功能,比如:
理解自定义 ClassLoader 有助于我们理解这些系统程序和框架,如 Tomat, JSP, OSGI,在业务需要的时候,也可以借助自定义 ClassLoader 实现动态灵活的功能。
下面,我们首先来进一步理解 Java 加载类的过程,理解类 ClassLoader 和 Class.forName,介绍一个简单的应用,然后我们探讨如何实现自定义 ClassLoader,演示如何利用它实现热部署。
类加载的基本机制和过程
运行 Java 程序,就是执行 java 这个命令,指定包含 main 方法的完整类名,以及一个 classpath,即类路径。类路径可以有多个,对于直接的 class 文件,路径是 class 文件的根目录,对于 jar 包,路径是 jar 包的完整名称(包括路径和 jar 包名)。
Java 运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找,如果是 class 文件的根目录,则直接查看是否有对应的子目录及文件,如果是 jar 文件,则首先在内存中解压文件,然后再查看是否有对应的类。
负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是 Class 对象。类加载器不是只有一个,一般程序运行时,都会有三个:
这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader 的父亲是 Extension ClassLoader,Extension 的父亲是 Bootstrap ClassLoader,注意不是父子继承关系,而是父子委派关系,子 ClassLoader 有一个变量 parent 指向父 ClassLoader,在子 ClassLoader 加载类时,一般会首先通过父 ClassLoader 加载,具体来说,在加载一个类时,基本过程是:
这个过程一般被称为 "双亲委派" 模型,即优先让父 ClassLoader 去加载。为什么要先让父 ClassLoader 去加载呢?这样,可以避免 Java 类库被覆盖的问题,比如用户程序也定义了一个类 java.lang.String,通过双亲委派,java.lang.String 只会被 Bootstrap ClassLoader 加载,避免自定义的 String 覆盖 Java 类库的定义。需要了解的是,"双亲委派" 虽然是一般模型,但也有一些例外,比如:
一个程序运行时,会创建一个 Application ClassLoader,在程序中用到 ClassLoader 的地方,如果没有指定,一般用的都是这个 ClassLoader,所以,这个 ClassLoader 也被称为系统类加载器 (System ClassLoader)。
下面,我们来具体看下表示类加载器的类 - ClassLoader。
理解 ClassLoader
基本用法
类 ClassLoader 是一个抽象类,Application ClassLoader 和 Extension ClassLoader 的具体实现类分别是 sun.misc.Launcher$AppClassLoader 和 sun.misc.Launcher$ExtClassLoader,Bootstrap ClassLoader 不是由 Java 实现的,没有对应的类。
每个 Class 对象都有一个方法,可以获取实际加载它的 ClassLoader,方法是:
- publicClassLoader getClassLoader()
ClassLoader 有一个方法,可以获取它的父 ClassLoader:
- public finalClassLoader getParent()
如果 ClassLoader 是 Bootstrap ClassLoader,返回值为 null。
比如:
- public class ClassLoaderDemo {
- public static void main(String[] args) {
- ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
- while(cl !=null) {
- System.out.println(cl.getClass().getName());
- cl = cl.getParent();
- }
- System.out.println(String.class.getClassLoader());
- }
- }
输出为:
- sun.misc.Launcher$AppClassLoader
- sun.misc.Launcher$ExtClassLoader
- null
ClassLoader 有一个静态方法,可以获取默认的系统类加载器:
- public staticClassLoader getSystemClassLoader()
ClassLoader 中有一个主要方法,用于加载类:
- publicClass loadClass(String name)throwsClassNotFoundException
比如:
- ClassLoader cl = ClassLoader.getSystemClassLoader();
- try {
- Classcls = cl.loadClass("java.util.ArrayList");
- ClassLoader actualLoader = cls.getClassLoader();
- System.out.println(actualLoader);
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
需要说明的是,由于委派机制,Class 的 getClassLoader() 方法返回的不一定是调用 loadClass 的 ClassLoader,比如,上面代码中,java.util.ArrayList 实际由 BootStrap ClassLoader 加载,所以返回值就是 null。
ClassLoader vs Class.forName
在反射一节,我们介绍过 Class 的两个静态方法 forName:
- public staticClass forName(String className)
- public staticClass forName(String name,booleaninitialize, ClassLoader loader)
第一个方法使用系统类加载器加载,第二个指定 ClassLoader,参数 initialize 表示,加载后,是否执行类的初始化代码 (如 static 语句块),没有指定默认为 true。
ClassLoader 的 loadClass 方法与 forName 方法都可以加载类,它们有什么不同呢?基本是一样的,不过,有一个不同,ClassLoader 的 loadClass 不会执行类的初始化代码,看个例子:
- public class CLInitDemo {
- public static class Hello {
- static {
- System.out.println("hello");
- }
- };
- public static void main(String[] args) {
- ClassLoader cl = ClassLoader.getSystemClassLoader();
- String className = CLInitDemo.class.getName() + "$Hello";
- try {
- Classcls = cl.loadClass(className);
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- }
使用 ClassLoader 加载静态内部类 Hello,Hello 有一个 static 语句块,输出 "hello",运行该程序,类被加载了,但没有任何输出,即 static 语句块没有被执行。如果将 loadClass 的语句换为:
- Class<?> cls = Class.forName(className);
则 static 语句块会被执行,屏幕将输出 "hello"。
实现代码
我们来看下 ClassLoader 的 loadClass 代码,以进一步理解其行为:
- publicClass loadClass(String name)throws ClassNotFoundException {
- returnloadClass(name,false);
- }
它调用了另一个 loadClass 方法,其主要代码为 (省略了一些代码,加了注释,以便于理解):
- protectedClass loadClass(String name,boolean resolve)
- throws ClassNotFoundException {
- synchronized (getClassLoadingLock(name)) {
- // 首先,检查类是否已经被加载了Class c = findLoadedClass(name);
- if(c ==null) {
- //没被加载,先委派父ClassLoader或BootStrap ClassLoader去加载
- try {
- if(parent !=null) {
- //委派父ClassLoader,resolve参数固定为falsec = parent.loadClass(name,false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- //没找到,捕获异常,以便尝试自己加载
- }
- if(c ==null) {
- // 自己去加载,findClass才是当前ClassLoader的真正加载方法c = findClass(name);
- }
- }
- if (resolve) {
- // 链接,执行static语句块
- resolveClass(c);
- }
- return c;
- }
- }
参数 resolve 类似 Class.forName 中的参数 initialize,可以看出,其默认值为 false,即使通过自定义 ClassLoader 重写 loadClass,设置 resolve 为 true,它调用父 ClassLoader 的时候,传递的也是固定的 false。
findClass 是一个 protected 方法,类 ClassLoader 的默认实现就是抛出 ClassNotFoundException,子类应该重写该方法,实现自己的加载逻辑,后文我们会看个具体例子。
类加载应用 - 可配置的策略
可以通过 ClassLoader 的 loadClass 或 Class.forName 自己加载类,但什么情况需要自己加载类呢?
很多应用使用面向接口的编程,接口具体的实现类可能有很多,适用于不同的场合,具体使用哪个实现类在配置文件中配置,通过更改配置,不用改变代码,就可以改变程序的行为,在设计模式中,这是一种策略模式,我们看个简单的示例。
定义一个服务接口 IService:
- public interface IService {
- public void action();
- }
客户端通过该接口访问其方法,怎么获得 IService 实例呢?查看配置文件,根据配置的实现类,自己加载,使用反射创建实例对象,示例代码为:
- public class ConfigurableStrategyDemo {
- public static IService createService() {
- try {
- Properties prop =new Properties();
- String fileName = "data/c87/config.properties";
- prop.load(new FileInputStream(fileName));
- String className = prop.getProperty("service");
- Classcls = Class.forName(className);
- return (IService) cls.newInstance();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
- public static void main(String[] args) {
- IService service = createService();
- service.action();
- }
- }
config.properties 的内容示例为:
- service=shuo.laoma.dynamic.c87.ServiceB
代码比较简单,就不赘述了。
自定义 ClassLoader
基本用法
Java 类加载机制的强大之处在于,我们可以创建自定义的 ClassLoader,自定义 ClassLoader 是 Tomcat 实现应用隔离、支持 JSP,OSGI 实现动态模块化的基础。
怎么自定义呢?一般而言,继承类 ClassLoader,重写 findClass 就可以了。怎么实现 findClass 呢?使用自己的逻辑寻找 class 文件字节码的字节形式,找到后,使用如下方法转换为 Class 对象:
- protected finalClass defineClass(String name,byte[] b,intoff,intlen)
name 表示类名,b 是存放字节码数据的字节数组,有效数据从 off 开始,长度为 len。
看个例子:
- public classMyClassLoaderextends ClassLoader {
- private static finalString BASE_DIR = "data/c87/";
- @Override
- protectedClass findClass(String name)throws ClassNotFoundException {
- String fileName = name.replaceAll("\\.", "/");
- fileName = BASE_DIR + fileName + ".class";
- try {
- byte[] bytes = BinaryFileUtils.readFileToByteArray(fileName);
- returndefineClass(name, bytes, 0, bytes.length);
- } catch (IOException ex) {
- throw newClassNotFoundException("failed to load class " + name, ex);
- }
- }
- }
MyClassLoader 从 BASE_DIR 下的路径中加载类,它使用了我们在 57 节介绍的 BinaryFileUtils 读取文件,转换为 byte 数组。MyClassLoader 没有指定父 ClassLoader,默认是系统类加载器,即 ClassLoader.getSystemClassLoader() 的返回值,不过,ClassLoader 有一个可重写的构造方法,可以指定父 ClassLoader:
- protectedClassLoader(ClassLoader parent)
用途
MyClassLoader 有什么用呢?将 BASE_DIR 加到 classpath 中不就行了,确实可以,这里主要是演示基本用法,实际中,可以从 Web 服务器、数据库或缓存服务器获取 bytes 数组,这就不是系统类加载器能做到的了。
不过,不把 BASE_DIR 放到 classpath 中,而是使用 MyClassLoader 加载,确实有一个很大的好处,可以创建多个 MyClassLoader,对同一个类,每个 MyClassLoader 都可以加载一次,得到同一个类的不同 Class 对象,比如:
- MyClassLoader cl1 =new MyClassLoader();
- String className = "shuo.laoma.dynamic.c87.HelloService";
- Classclass1 = cl1.loadClass(className);
- MyClassLoader cl2 =new MyClassLoader();
- Classclass2 = cl2.loadClass(className);
- if(class1 != class2) {
- System.out.println("different classes");
- }
cl1 和 cl2 是两个不同的 ClassLoader,class1 和 class2 对应的类名一样,但它们是不同的对象。
这到底有什么用呢?
下面,我们来具体看热部署的示例。
自定义 ClassLoader 的应用 - 热部署
所谓热部署,就是在不重启应用的情况下,当类的定义,即字节码文件修改后,能够替换该 Class 创建的对象,怎么做到这一点呢?我们利用 MyClassLoader,看个简单的示例。
我们使用面向接口的编程,定义一个接口 IHelloService:
- public interface IHelloService {
- public void sayHello();
- }
实现类是 shuo.laoma.dynamic.c87.HelloImpl,class 文件放到 MyClassLoader 的加载目录中。
演示类是 HotDeployDemo,它定义了以下静态变量:
- private static finalString CLASS_NAME = "shuo.laoma.dynamic.c87.HelloImpl";
- private static finalString FILE_NAME = "data/c87/"
- +CLASS_NAME.replaceAll("\\.", "/")+".class";
- private static volatileIHelloService helloService;
CLASS_NAME 表示实现类名称,FILE_NAME 是具体的 class 文件路径,helloService 是 IHelloService 实例。
当 CLASS_NAME 代表的类字节码改变后,我们希望重新创建 helloService,反映最新的代码,怎么做呢?先看用户端获取 IHelloService 的方法:
- public static IHelloService getHelloService() {
- if(helloService !=null) {
- return helloService;
- }
- synchronized(HotDeployDemo.class) {
- if(helloService ==null) {
- helloService = createHelloService();
- }
- return helloService;
- }
- }
这是一个单例模式,createHelloService() 的代码为:
- private static IHelloService createHelloService() {
- try {
- MyClassLoader cl =new MyClassLoader();
- Classcls = cl.loadClass(CLASS_NAME);
- if(cls !=null) {
- return (IHelloService) cls.newInstance();
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return null;
- }
它使用 MyClassLoader 加载类,并利用反射创建实例,它假定实现类有一个 public 无参构造方法。
在调用 IHelloService 的方法时,客户端总是先通过 getHelloService 获取实例对象,我们模拟一个客户端线程,它不停的获取 IHelloService 对象,并调用其方法,然后睡眠 1 秒钟,其代码为:
- public static void client() {
- Thread t =new Thread() {
- @Override
- public void run() {
- try {
- while(true) {
- IHelloService helloService = getHelloService();
- helloService.sayHello();
- Thread.sleep(1000);
- }
- } catch (InterruptedException e) {
- }
- }
- };
- t.start();
- }
怎么知道类的 class 文件发生了变化,并重新创建 helloService 对象呢?我们使用一个单独的线程模拟这一过程,代码为:
- public static void monitor() {
- Thread t =new Thread() {
- private longlastModified =new File(FILE_NAME).lastModified();
- @Override
- public void run() {
- try {
- while(true) {
- Thread.sleep(100);
- longnow =new File(FILE_NAME).lastModified();
- if(now != lastModified) {
- lastModified = now;
- reloadHelloService();
- }
- }
- } catch (InterruptedException e) {
- }
- }
- };
- t.start();
- }
我们使用文件的最后修改时间来跟踪文件是否发生了变化,当文件修改后,调用 reloadHelloService() 来重新加载,其代码为:
- public static void reloadHelloService() {
- helloService = createHelloService();
- }
就是利用 MyClassLoader 重新创建 HelloService,创建后,赋值给 helloService,这样,下次 getHelloService() 获取到的就是最新的了。
在主程序中启动 client 和 monitor 线程,代码为:
- public static void main(String[] args) {
- monitor();
- client();
- }
在运行过程中,替换 HelloImpl.class,可以看到行为会变化,为便于演示,我们在 data/c87/shuo/laoma/dynamic/c87 / 目录下准备了两个不同的实现类 HelloImpl_origin.class 和 HelloImpl_revised.class,在运行过程中替换,会看到输出不一样,如下图所示:
使用 cp 命令修改 HelloImpl.class,如果其内容与 HelloImpl_origin.class 一样,输出为 "hello",如果与 HelloImpl_revised.class 一样,输出为 "hello revised"。
完整的代码和数据在 github 上,文末有链接。
小结
本节探讨了 Java 中的类加载机制,包括 Java 加载类的基本过程,类 ClassLoader 的用法,以及如何创建自定义的 ClassLoader,探讨了两个简单应用示例,一个通过动态加载实现可配置的策略,另一个通过自定义 ClassLoader 实现热部署。
从 84 节到本节,我们探讨了 Java 中的多个动态特性,包括反射、注解、动态代理和类加载器,作为应用程序员,大部分用的都比较少,用的较多的就是使用框架和库提供的各种注解了,但这些特性大量应用于各种系统程序、框架、和库中,理解这些特性有助于我们更好的理解它们,也可以在需要的时候自己实现动态、通用、灵活的功能。
在注解一节,我们提到,注解是一种声明式编程风格,它提高了 Java 语言的表达能力,日常编程中一种常见的需求是文本处理,在计算机科学中,有一种技术大大提高了文本处理的表达能力,那就是正则表达式,大部分编程语言都有对它的支持,它有什么强大功能呢?
(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,位于包 shuo.laoma.dynamic.c87 下)
----------------
未完待续,查看最新文章,敬请关注微信公众号 "老马说编程"(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索 Java 编程及计算机技术的本质。用心原创,保留所有版权。
来源: http://www.cnblogs.com/swiftma/p/6901301.html