在用Java开发web应用程序的UI时,以下工作流程可能很常见:
- 对某个源文件进行更改;
- 重新编译项目;
- 重启服务器;
- 测试
每次进行更改时重新启动服务器并不理想。即使你的应用程序启动很快。
如果我们能够在运行时重新加载类定义,则可能并不总是需要重新启动。
在这篇文章中,将展示如何在运行时重新加载单个类的定义。
类重载器
我们需要一个可以在运行时加载和重新加载单个类定义的类。我们将把类命名为ClassReloader
。让我们从测开始。
Subject类
在我们的测试中,我们将使用重载器加载以下类定义:
package reloader;
public class Subject {
@Override
public String toString() {
return "I'm the original!";
}
}
然后,我们将创建加载类的实例。我们希望它的toString
方法返回“我是原始的!”值。
接下来,我们将重新加载Subject
类型的第二个版本:
package reloader;
public class Subject {
@Override
public String toString() {
return "Reloading successful!";
}
}
然后,我们将创建一个新的重载类定义实例。我们希望它的toString现在返回“Reloading successful!
”值。
创建测试类的十六进制转储
我们手动编译Subject类的两个版本。接下来我们:
- 使用xxd工具创建类文件的十六进制转储;
- 在我们的测试中使用文本块来存储十六进制转储。
以下是我们测试类的当前迭代:
public class ClassReloaderTest {
private static final String ORIGINAL = """
cafebabe0000004100110a000200030700040c000500060100106a617661\
2f6c616e672f4f626a6563740100063c696e69743e010003282956080008\
01001149276d20746865206f726967696e616c2107000a01001072656c6f\
616465722f5375626a656374010004436f646501000f4c696e654e756d62\
65725461626c65010008746f537472696e6701001428294c6a6176612f6c\
616e672f537472696e673b01000a536f7572636546696c6501000c537562\
6a6563742e6a617661002100090002000000000002000100050006000100\
0b0000001d00010001000000052ab70001b100000001000c000000060001\
000000030011000d000e0001000b0000001b00010001000000031207b000\
000001000c000000060001000000060001000f000000020010\
""";
private static final String UPDATED = """
cafebabe0000004100110a000200030700040c000500060100106a617661\
2f6c616e672f4f626a6563740100063c696e69743e010003282956080008\
01001552656c6f6164696e67207375636365737366756c2107000a010010\
72656c6f616465722f5375626a656374010004436f646501000f4c696e65\
4e756d6265725461626c65010008746f537472696e6701001428294c6a61\
76612f6c616e672f537472696e673b01000a536f7572636546696c650100\
0c5375626a6563742e6a6176610021000900020000000000020001000500\
060001000b0000001d00010001000000052ab70001b100000001000c0000\
00060001000000030011000d000e0001000b0000001b0001000100000003\
1207b000000001000c000000060001000000060001000f000000020010\
""";
}
ORIGINAL文本块包含我们类的第一个版本。第二个是UPDATED文本块。
试验方法
使用Subject类的十六进制转储,我们编写以下测试方法:
@Test(description = """
It should be able to load and reload the subject class.
""")
public void testCase01() throws Exception {
Path classOutput;
classOutput = Files.createTempDirectory("reloader-");
ClassReloader reloader;
reloader = new ClassReloader(classOutput);
try {
Path packageDir;
packageDir = classOutput.resolve("reloader");
Files.createDirectories(packageDir);
Path classFile;
classFile = packageDir.resolve("Subject.class");
HexFormat hexFormat;
hexFormat = HexFormat.of();
Files.write(classFile, hexFormat.parseHex(ORIGINAL));
assertEquals(execute(reloader), "I'm the original!");
Files.write(classFile, hexFormat.parseHex(UPDATED));
assertEquals(execute(reloader), "Reloading successful!");
} finally {
deleteRecursively(classOutput);
}
}
我们首先创建一个临时目录,表示Java项目的“类输出”。例如,在Maven项目中,它将表示target/classes目录。
我们使用此目录创建ClassReloader实例:
Path classOutput;
classOutput = Files.createTempDirectory("reloader-");
ClassReloader reloader;
reloader = new ClassReloader(classOutput);
我们的重载程序将尝试在此目录中查找.class文件。
接下来,使用ORIGINAL十六进制转储,我们创建Subject类的类文件:
Path packageDir;
packageDir = classOutput.resolve("reloader");
Files.createDirectories(packageDir);
Path classFile;
classFile = packageDir.resolve("Subject.class");
HexFormat hexFormat;
hexFormat = HexFormat.of();
Files.write(classFile, hexFormat.parseHex(ORIGINAL));
它首先创建表示类包的目录。
然后,它使用HexFormat
类将十六进制转储转换为字节数组。
接下来,我们写出第一个断言:
assertEquals(execute(reloader), "I'm the original!");
其中execute
方法由下式给出:
private String execute(ClassReloader reloader) throws Exception {
Class<?> clazz;
clazz = reloader.loadClass("reloader.Subject");
Constructor<?> constructor;
constructor = clazz.getConstructor();
Object instance;
instance = constructor.newInstance();
return instance.toString();
}
使用我们的重载器,它试图通过给出Subject类的二进制名称来加载它。
假设加载成功,则:
- 创建类的新实例;
- 返回对象的toString值。
接下来,测试模拟我们类的重新编译:
Files.write(classFile, hexFormat.parseHex(UPDATED));
assertEquals(execute(reloader), "Reloading successful!");
它尝试重新加载类定义,并验证新实例是否返回预期的toString值。
实施
下面是一个使我们的测试通过的实现:
public class ClassReloader {
private final Path classOutput;
public ClassReloader(Path classOutput) {
this.classOutput = classOutput;
}
public final Class<?> loadClass(String binaryName) throws ClassNotFoundException {
ThisLoader loader;
loader = new ThisLoader();
return loader.loadClass(binaryName);
}
private class ThisLoader extends ClassLoader {
public ThisLoader() {
super(null); // no parent
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String fileName;
fileName = name.replace('.', File.separatorChar);
fileName += ".class";
Path path;
path = classOutput.resolve(fileName);
byte[] bytes;
bytes = Files.readAllBytes(path);
return defineClass(name, bytes, 0, bytes.length);
} catch (NoSuchFileException e) {
ClassLoader systemLoader;
systemLoader = ClassLoader.getSystemClassLoader();
return systemLoader.loadClass(name);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
}
在loadClass方法中,我们创建了内部ClassLoader子类的新实例。
此子类重写findClass方法:
- 它试图从classOutput目录读取类定义;
- 如果由于文件不存在而失败,则将其委托给系统类加载器;
- 如果它因另一个I/O错误而失败,它会将异常作为
ClassNotFoundException
重新抛出。
如前所述,此实现使我们的测试通过。
结论
我们的ClassLoader
实现能够在运行时重新加载单个Java类定义。
它急切地在每次调用时加载类定义,这并不理想。因此,它可能不是Java web应用程序开发过程中使用的最佳候选者。
但我希望它表明,在运行时重新加载单个类定义可以用几行Java代码完成。
您可以在这个GitHub(https://github.com/objectos/blog-examples/tree/main/2024/02/18)找到示例的源代码。
原文地址:https://www.objectos.com.br/blog/reloading-a-java-class-definition-at-runtime.html
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/3001.html
暂无评论