24.5 自定义ClassLoader的应用:热部署

24.5 自定义ClassLoader的应用:热部署

所谓热部署,就是在不重启应用的情况下,当类的定义即字节码文件修改后,能够替换该Class创建的对象,怎么做到这一点呢?我们利用MyClassLoader,看个简单的示例。

我们使用面向接口的编程,定义一个接口IHelloService:

1
2
3
public interface IHelloService {
public void sayHello();
}

实现类是shuo.laoma.dynamic.c87.HelloImpl, class文件放到MyClassLoader的加载目录中。

演示类是HotDeployDemo,它定义了以下静态变量:

1
2
3
4
private static final String CLASS_NAME = "shuo.laoma.dynamic.c87.HelloImpl";
private static final String FILE_NAME = "data/c87/"
+CLASS_NAME.replaceAll("\\.", "/")+".class";
private static volatile IHelloService helloService;

CLASS_NAME表示实现类名称,FILE_NAME是具体的class文件路径,helloService是IHelloService实例。

当CLASS_NAME代表的类字节码改变后,我们希望重新创建helloService,反映最新的代码,怎么做呢?先看用户端获取IHelloService的方法:

1
2
3
4
5
6
7
8
9
10
11
public static IHelloService getHelloService() {
if(helloService ! = null) {
return helloService;
}
synchronized (HotDeployDemo.class) {
if(helloService == null) {
helloService = createHelloService();
}
return helloService;
}
}

这是一个单例模式,createHelloService()的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
private static IHelloService createHelloService() {
try {
MyClassLoader cl = new MyClassLoader();
Class<? > cls = cl.loadClass(CLASS_NAME);
if(cls ! = null) {
return (IHelloService) cls.newInstance();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

它使用MyClassLoader加载类,并利用反射创建实例,它假定实现类有一个public无参构造方法。

在调用IHelloService的方法时,客户端总是先通过getHelloService获取实例对象,我们模拟一个客户端线程,它不停地获取IHelloService对象,并调用其方法,然后睡眠1秒钟,其代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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对象呢?我们使用一个单独的线程模拟这一过程,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void monitor() {
Thread t = new Thread() {
private long lastModified = new File(FILE_NAME).lastModified();
@Override
public void run() {
try {
while(true) {
Thread.sleep(100);
long now = new File(FILE_NAME).lastModified();
if(now ! = lastModified) {
lastModified = now;
reloadHelloService();
}
}
} catch (InterruptedException e) {
}
}
};
t.start();
}

我们使用文件的最后修改时间来跟踪文件是否发生了变化,当文件修改后,调用reloadHelloService()来重新加载,其代码为:

1
2
3
public static void reloadHelloService() {
helloService = createHelloService();
}

就是利用MyClassLoader重新创建HelloService,创建后,赋值给helloService,这样,下次getHelloService()获取到的就是最新的了。

在主程序中启动client和monitor线程,代码为:

1
2
3
4
public static void main(String[] args) {
monitor();
client();
}

在运行过程中,替换HelloImpl.class,可以看到行为会变化,为便于演示,我们在data/c87/shuo/laoma/dynamic/c87/目录下准备了两个不同的实现类:HelloImpl_origin.class和HelloImpl_revised. class,在运行过程中替换,会看到输出不一样,如图24-1所示。

epub_923038_143

图24-1 动态替换实现类示例

使用cp命令修改HelloImpl.class,如果其内容与HelloImpl_origin.class一样,输出为”hello”;如果与HelloImpl_revised.class一样,输出为”hello revised”。

完整的代码和数据在github上,地址为https://github.com/swiftma/program-logic ,位于包shuo.laoma.dynamic.c87下。

本章介绍了Java中的类加载机制,包括Java加载类的基本过程,类ClassLoader的用法,以及如何创建自定义的ClassLoader,探讨了两个简单应用示例,一个通过动态加载实现了可配置的策略,另一个通过自定义ClassLoader实现了热部署。

需要说明的是,Java 9引入了模块的概念。在模块化系统中,类加载的过程有一些变化,扩展类的目录被删除掉了,原来的扩展类加载器没有了,增加了一个平台类加载器(Platform Class Loader),角色类似于扩展类加载器,它分担了一部分启动类加载器的职责,另外,加载的顺序也有一些变化,限于篇幅,我们就不探讨了。

从第21章到本章,我们探讨了Java中的多个动态特性,包括反射、注解、动态代理和类加载器,作为应用程序员,大部分用得都比较少,用得较多的就是使用框架和库提供的各种注解了,但这些特性大量应用于各种系统程序、框架和库中,理解这些特性有助于我们更好地理解它们,也可以在需要的时候自己实现动态、通用、灵活的功能。

在注解一章,我们提到,注解是一种声明式编程风格,它提高了Java语言的表达能力,日常编程中一种常见的需求是文本处理,在计算机科学中,有一种技术大大提高了文本处理的表达能力,那就是正则表达式,大部分编程语言都有对它的支持,它有什么强大功能呢?让我们下一章探讨。