测试清理器清理
Roger Riggs 于 2022 年 5 月 27 日
在 用清理器替换终结器 中,堆对象封装的资源清理使用两种互补机制来安排:try-with-resources 和 清理器。需要某种清理的资源包括安全敏感数据和堆外资源,例如文件描述符或本机句柄。清理应该始终发生,并且应该在资源不再活动时尽快发生。
清理器取代终结器后出现的一个问题是如何测试清理是否正常工作。对于 try-with-resources,测试非常简单,我们需要验证的状态在一个对象中,该对象可以访问或可以被测试访问,并且何时检查它已完成很清楚(在关闭之后)。
- 在 try-with-resources 语句中,创建一个对 对象 的引用
- 提取对要清理的封装状态数据的引用
- 退出 try-with-resources 语句,隐式调用
close
- 检查清理是否已发生
public void testAutoClose() {
char[] origChars = "myPrivateData".toCharArray();
char[] implChars;
try (SensitiveData data = new SensitiveData(origChars)) {
// Save the sensitiveData char array
implChars = (char[]) getField(SensitiveData.class,
"sensitiveData", data);
}
// After close, was it cleared?
char[] zeroChars = new char[implChars.length];
assertEquals(implChars, zeroChars,
"SensitiveData chars not zero: ");
}
相当简单,下面的 getField
实用程序方法用于从 SensitiveData.sensitiveData
中获取要清除的私有字符数组。
import java.lang.reflect.Field;
/**
* Get an object from a named field.
*/
static Object getField(Class<?> clazz,
String fieldName, Object instance) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(instance);
} catch (NoSuchFieldException | IllegalAccessException ex) {
throw new RuntimeException("field unknown or not accessible");
}
}
测试 SensitiveData 的清理器
验证不可达情况下的清理更有趣一些。清理 函数不会在垃圾收集器确定 对象 不可达之后运行。设置相同,检查数组是否已清除也是相同的。try-with-resources 被替换为清除对 SensitiveData
的引用,以便它可以被垃圾回收。
- 创建并保存对要清理的 对象 的引用
- 提取对要清理的封装数据的引用
- 删除对 对象 的引用
- 请求垃圾收集器运行
- 轮询数组,检查清理是否已完成
public void testCharArray() {
final char[] origChars = "myPrivateData".toCharArray();
SensitiveData data = new SensitiveData(origChars);
// A reference to sensitiveData char array
char[] implChars = (char[]) getField(SensitiveData.class,
"sensitiveData", data);
data = null; // Remove reference
char[] zeroChars = new char[implChars.length];
for (int i = 10; i > 0; i--) {
System.gc();
try {
Thread.sleep(10L);
} catch (InterruptedException ie) { }
if (Arrays.equals(implChars, zeroChars))
break; // break as soon as cleared
}
// Check and report any errors
assertEquals(implChars, zeroChars,
"After GC, chars not zero");
}
与上面的 AutoCloseable
案例一样,内部 sensitiveData
字符数组由测试保存。在对 SensitiveData
对象的引用设置为 null 之后,System.gc()
用于启动垃圾收集器,然后检查数组是否为零。垃圾收集器并行运行,在调用 System.gc()
之后可能需要一些时间,垃圾收集器才能确定 对象 不再被引用,并且 清理器 被通知调用 Cleanable.clean()
,从而调用 清理函数。
此测试代码直接确认数组已清除。只要要清理的状态对测试可见,它就能很好地工作,但对于其他正在测试的案例和类,状态可能并不总是可见或可访问的。
堆外资源的替代测试
对于某些类型的资源清理,测试无法直接观察到清理是否已发生。例如,如果资源是本机内存地址或句柄,清理函数 可能直接处理资源,并且测试可能无法观察到资源是否已释放或清除。
最好的办法是观察 清理函数 是否已被调用并已完成。要了解它是如何工作的,我们需要了解 清理器 如何确定何时调用 清理函数。
一个 清理器 是一个线程,它等待通知注册的 对象 不再可达,然后调用相应的 清理函数。当 清理函数 与 清理器 注册时,对象及其 清理函数 通过创建和返回 Cleanable 来链接。通常,Cleanable 对象保存在 对象 中,就像在 SensitiveData
示例中一样,以便 close
方法可以调用 Cleanable.clean
来调用 清理函数 并删除注册。
Cleanable 的实现是 对象 的 PhantomReference
。 PhantomReference
不会使对象保持活动状态,并且可以查询以了解对象是否仍然活动。在正常的垃圾回收过程中,当 对象 变得不可达时,Cleanable 会被排队等待 清理器 线程处理。在它被清理之前,Cleanable 会被 清理器 引用,并且不会被垃圾回收。在它的 清理函数 被调用之后,Cleanable 本身会被释放并被垃圾回收。
使用与触发清理相同的基于引用的技术,测试可以监控 Cleanable,并在 Cleanable 变得不再被引用时知道它已完成。测试从 SensitiveData.cleanable
字段中检索 Cleanable,并创建自己的 PhantomReference
来使用自己的 ReferenceQueue
轮询实用程序来监控它。
- 创建并保存对要清理的 对象 的引用
- 提取对保存清理函数的 Cleanable 的引用。
- 检查清理是否在删除对对象的引用之前发生
- 删除对 对象 的引用
- 等待 Cleanable 不再被引用
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public void testCleanable() {
final char[] origChars = "myPrivateData".toCharArray();
SensitiveData data = new SensitiveData(origChars);
// Extract a reference to the Cleanable
Cleanable cleanable = (Cleaner.Cleanable)
getField(SensitiveData.class, "cleanable", data);
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> ref =
new PhantomReference<>(cleanable, queue);
cleanable = null;
// Only the Cleaner will have a strong
// reference to the Cleanable
// Check that the cleanup does not happen
// before the reference is cleared.
assertNull(waitForReference(queue),
"SensitiveData cleaned prematurely");
data = null; // Remove the reference
assertEquals(waitForReference(queue), ref,
"After GC, not cleaned");
}
此测试使用实用程序方法 waitForReference
来调用垃圾回收并等待引用被排队。调用者检查它是否是对象的预期 PhantomReference
。
/**
* Wait for a Reference to be enqueued.
* Returns null if no reference is queued within 0.1 seconds
*/
static Reference<?> waitForReference(ReferenceQueue<Object> queue) {
Objects.requireNonNull(queue);
for (int i = 10; i > 0; i--) {
System.gc();
try {
var r = queue.remove(10L);
if (r != null) {
return r;
}
} catch (InterruptedException ie) {
// ignore, the loop will try again
}
};
return null;
}
这种测试清理函数的技术依赖于对 SensitiveData
类及其使用 清理器 管理的 Cleanable 对象的实现的了解。
测试 Cleanable 的初始设置(在对象引用设置为 null 之前)验证清理不会过早地调用,如果这样,这很可能是测试或实现中的错误。
这种技术(等待 Cleanable 被调用并变得不可达)对于 清理函数 是一个简单的 lambda 还是一个显式的记录类、嵌套类或顶级类是有效的。虽然它没有直接验证清理,但它确实验证了 清理函数 已被调用并已完成。
这些只是编写测试的几种可能方法,还有许多其他方法利用了与正在测试的类的协作。通过重构状态或允许测试破坏类的封装,清理函数 和测试都可以具有确认清理发生以及在预期时间发生所需的可见性。
SensitiveData
示例代码和 SensistiveDataTest
测试代码可在 SensitiveData Gist 中找到。测试使用 `TestNG` 进行测试断言。
最初发布在 这里。