我正在创作一个java库。一些旨在由库用户使用的类保存本机系统资源(通过JNI)。我想确保用户“处置”这些对象,因为它们很重,并且在测试套件中它们可能导致测试用例之间的泄漏(例如,我需要确保TearDown
将处置)。为此我使Java类实现了AutoCloseable,但这似乎不够,或者我没有正确使用它:
try-with-resources
语句(我使用JUnit5
和Mockito
),因为“资源”不是短暂的 - 它是测试夹具的一部分。finalize()
并测试封闭,但事实证明finalize()
甚至没有被称为(Java10)。这也被标记为已弃用,我确信这个想法不会受到诟病。这是怎么做到的?要清楚,如果他们不在我的对象上调用close()
,我希望应用程序的测试(使用我的库)失败。
编辑:添加一些代码,如果它有帮助。它并不多,但这正是我想要做的。
@SuppressWarnings("deprecation") // finalize() provided just to assert closure (deprecated starting Java 9)
@Override
protected final void finalize() throws Throwable {
if (nativeHandle_ != 0) {
// TODO finalizer is never called, how to assert that close() gets called?
throw new AssertionError("close() was not called; native object leaking");
}
}
编辑2,赏金的结果感谢所有的回复,一半的赏金被自动授予。我的结论是,对于我的情况,最好尝试涉及Cleaner
的解决方案。但是看起来,虽然已经注册,但是不会调用清理操作。我问了一个跟进问题here。
这篇文章没有直接回答你的问题,但提供了不同的观点。
让您的客户始终致电close
的一种方法是让他们免除此责任。
使用模板模式。
你提到你正在使用TCP,所以让我们假设你有一个TcpConnection
类,它有一个close()
方法。
让我们定义TcpConnectionOpertaions
界面:
public interface TcpConnectionOperations {
<T> T doWithConnection(TcpConnectionAction<T> action);
}
并实现它:
public class TcpConnectionTemplate implements TcpConnectionOperations {
@Override
public <T> T doWithConnection(TcpConnectionAction<T> action) {
try (TcpConnection tcpConnection = getConnection()) {
return action.doWithConnection(tcpConnection);
}
}
}
TcpConnectionAction
只是一个回调,没什么特别的。
public interface TcpConnectionAction<T> {
T doWithConnection(TcpConnection tcpConnection);
}
TcpConnectionOperations
界面消费。例如:
String s = tcpConnectionOperations.doWithConnection(connection -> {
// do what we with with the connection
// returning to string for example
return connection.toString();
});
TcpConnection
TcpConnectionOperations
和模拟TcpConnections
并对它们进行断言如果资源的生命周期长于action
,则此方法可能无效。例如。客户端需要将资源保留更长时间。
然后你可能想深入了解ReferenceQueue
/ Cleaner
(自Java 9以来)和相关的API。
这种模式广泛用于Spring framework。
参见例如:
JdbcTemplate
TransactionTemplate
JmsTemplate
。这是某种pooling:
池是一组资源,可以随时使用,而不是在使用和获取时获取
Java中的一些池:
ConnectionPool
ThreadPoolExecutor
在实现池时,会提出几个问题:
close
d?当资源应该是close
d?
通常池提供明确的close
方法(它可能有不同的名称,但目的是相同的),它关闭所有持有的资源。
HikariDataSource#close
ConnectionPool#evictAll
“关闭并删除池中的所有空闲连接。”ConnectionPool#close
ThreadPoolExecutor#shutdown
如何在多个线程之间共享?
它取决于资源本身的种类。
通常,您希望确保只有一个线程访问一个资源。
这可以使用某种锁定来完成
请注意,此处提供的代码仅用于演示目的。它具有可怕的性能并违反了一些OOP原则。
IpAndPort.java
@Value
public class IpAndPort {
InetAddress address;
int port;
}
TcpConnection.java
@Data
public class TcpConnection {
private static final AtomicLong counter = new AtomicLong();
private final IpAndPort ipAndPort;
private final long instance = counter.incrementAndGet();
public void close() {
System.out.println("Closed " + this);
}
}
CachingTcpConnectionTemplate.java
public class CachingTcpConnectionTemplate implements TcpConnectionOperations {
private final Map<IpAndPort, TcpConnection> cache
= new HashMap<>();
private boolean closed;
public CachingTcpConnectionTemplate() {
System.out.println("Created new template");
}
@Override
public synchronized <T> T doWithConnectionTo(IpAndPort ipAndPort, TcpConnectionAction<T> action) {
if (closed) {
throw new IllegalStateException("Closed");
}
TcpConnection tcpConnection = cache.computeIfAbsent(ipAndPort, this::getConnection);
try {
System.out.println("Executing action with connection " + tcpConnection);
return action.doWithConnection(tcpConnection);
} finally {
System.out.println("Returned connection " + tcpConnection);
}
}
private TcpConnection getConnection(IpAndPort ipAndPort) {
return new TcpConnection(ipAndPort);
}
@Override
public synchronized void close() {
if (closed) {
throw new IllegalStateException("closed");
}
closed = true;
for (Map.Entry<IpAndPort, TcpConnection> entry : cache.entrySet()) {
entry.getValue().close();
}
System.out.println("Template closed");
}
}
Tests infrastructure
TcpConnectionOperationsParameterResolver.java
public class TcpConnectionOperationsParameterResolver implements ParameterResolver, AfterAllCallback {
private final CachingTcpConnectionTemplate tcpConnectionTemplate = new CachingTcpConnectionTemplate();
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType().isAssignableFrom(CachingTcpConnectionTemplate.class)
&& parameterContext.isAnnotated(ReuseTemplate.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return tcpConnectionTemplate;
}
@Override
public void afterAll(ExtensionContext context) throws Exception {
tcpConnectionTemplate.close();
}
}
ParameterResolver
和AfterAllCallback
来自JUnit。
@ReuseTemplate
是一个自定义注释
ReuseTemplate.java
:
@Retention(RetentionPolicy.RUNTIME)
public @interface ReuseTemplate {
}
最后测试:
@ExtendWith(TcpConnectionOperationsParameterResolver.class)
public class Tests2 {
private final TcpConnectionOperations tcpConnectionOperations;
public Tests2(@ReuseTemplate TcpConnectionOperations tcpConnectionOperations) {
this.tcpConnectionOperations = tcpConnectionOperations;
}
@Test
void google80() throws UnknownHostException {
tcpConnectionOperations.doWithConnectionTo(new IpAndPort(InetAddress.getByName("google.com"), 80), tcpConnection -> {
System.out.println("Using " + tcpConnection);
return tcpConnection.toString();
});
}
@Test
void google80_2() throws Exception {
tcpConnectionOperations.doWithConnectionTo(new IpAndPort(InetAddress.getByName("google.com"), 80), tcpConnection -> {
System.out.println("Using " + tcpConnection);
return tcpConnection.toString();
});
}
@Test
void google443() throws Exception {
tcpConnectionOperations.doWithConnectionTo(new IpAndPort(InetAddress.getByName("google.com"), 443), tcpConnection -> {
System.out.println("Using " + tcpConnection);
return tcpConnection.toString();
});
}
}
运行:
$ mvn test
输出:
Created new template
[INFO] Running Tests2
Executing action with connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Using TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Returned connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Executing action with connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Using TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Returned connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Executing action with connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Using TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Returned connection TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Closed TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=80), instance=1)
Closed TcpConnection(ipAndPort=IpAndPort(address=google.com/74.125.131.102, port=443), instance=2)
Template closed
这里的关键观察是连接被重用(参见“instance=
”)
这是可以做什么的过于简单的例子。当然,在现实世界中,联系并不是那么简单。池不应无限增长,连接只能保持特定的时间段等等。通常在后台有一些问题可以解决一些问题。
我没有看到如何在测试环境中使用
try-with-resources statement
(我使用JUnit5
和Mockito
),因为“资源”不是短暂的 - 它是测试夹具的一部分。
见Junit 5 User Guide. Extension model
像往常一样勤奋,我尝试实施
finalize()
并测试封闭,但事实证明finalize()
甚至没有被称为(Java10)。这也被标记为已弃用,我确信这个想法不会受到诟病。
你覆盖了finalize
,以便它抛出一个异常,但它们会被忽略。
如果finalize方法抛出未捕获的异常,则忽略该异常并终止该对象的终止。
你可以做的最好的事情是记录资源泄漏和close
资源
要清楚,如果他们不在我的对象上调用
close()
,我希望应用程序的测试(使用我的库)失败。
应用程序测试如何使用您的资源?他们使用new
运算符实例化它吗?如果是,那么我认为PowerMock可以帮助你(但我不确定)
如果您隐藏了某种工厂背后的资源实例化,那么您可以给应用程序测试一些模拟工厂
如果你有兴趣,你可以看看这个talk。这是俄语,但仍然可能有用(我的答案的一部分是基于这个演讲)。
如果我是你,我会做以下事情:
这也取决于你是否想在生产中使用这种机制 - 也许值得将这个功能添加到你的lib中,因为资源管理也是生产环境中的一个问题。在这种情况下,您不需要包装器,但可以使用此功能扩展当前类。您可以使用后台线程进行定期检查,而不是拆解。
关于参考类型,我推荐this link。建议将PhantomReferences用于资源清理。
如果您对测试的一致性感兴趣,只需将destroy()
注释标记的方法@AfterClass
添加到测试类中,然后关闭其中所有先前分配的资源。
如果您对允许保护资源不被关闭的方法感兴趣,则可以提供一种不向用户显式公开资源的方法。例如,您的代码可以控制资源生命周期并仅接受来自用户的Consumer<T>
。
如果你不能这样做,但仍然希望确保资源将被关闭,即使用户没有正确使用它,你将不得不做几件棘手的事情。您可以在sharedPtr
和resource
本身上拆分您的资源。然后将sharedPtr
暴露给用户并将其放入包裹在WeakReference
中的内部存储器中。因此,你将能够捕捉到GC移除sharedPtr
并在close()
上调用resource
的那一刻。请注意,resource
不得向用户公开。我准备了一个例子,它不是很准确,但希望它能表明这个想法:
public interface Resource extends AutoCloseable {
public int jniCall();
}
class InternalResource implements Resource {
public InternalResource() {
// Allocate resources here.
System.out.println("Resources were allocated");
}
@Override public int jniCall() {
return 42;
}
@Override public void close() {
// Dispose resources here.
System.out.println("Resources were disposed");
}
}
class SharedPtr implements Resource {
private final Resource delegate;
public SharedPtr(Resource delegate) {
this.delegate = delegate;
}
@Override public int jniCall() {
return delegate.jniCall();
}
@Override public void close() throws Exception {
delegate.close();
}
}
public class ResourceFactory {
public static Resource getResource() {
InternalResource resource = new InternalResource();
SharedPtr sharedPtr = new SharedPtr(resource);
Thread watcher = getWatcherThread(new WeakReference<>(sharedPtr), resource);
watcher.setDaemon(true);
watcher.start();
Runtime.getRuntime().addShutdownHook(new Thread(resource::close));
return sharedPtr;
}
private static Thread getWatcherThread(WeakReference<SharedPtr> ref, InternalResource resource) {
return new Thread(() -> {
while (!Thread.currentThread().isInterrupted() && ref.get() != null)
LockSupport.parkNanos(1_000_000);
resource.close();
});
}
}
通常,如果您可以可靠地测试资源是否已关闭,您可以自己关闭它。
首先要做的是为客户端提供方便的处理资源。使用Execute Around成语。
据我所知,在Java库中执行资源处理的唯一用途是java.security.AccessController.doPrivileged
,这是特殊的(资源是一个神奇的堆栈框架,你真的不想打开)。我相信Spring长期以来一直有一个急需的JDBC库。在Java 1.1使其变得模糊实用之后不久,我肯定使用了执行环节(当时不知道它被称为那个)。
库代码应该类似于:
@FunctionalInterface
public interface WithMyResource<R> {
R use(MyResource resource) throws MyException;
}
public class MyContext {
// ...
public <R> R doAction(Arg arg, WithMyResource<R> with) throws MyException {
try (MyResource resource = acquire(arg)) {
return with.use(resource);
}
}
(在正确的位置获取类型参数声明。)
客户端使用情况如下所示:
MyType myResult = yourContext.doContext(resource -> {
...blah...;
return ...thing...;
});
回到测试。即使被测试者从执行中渗出资源或其他一些机制可用,我们如何使测试变得容易?
显而易见的答案是,您为测试提供了执行解决方案。您需要提供一些执行周围使用API来验证已在范围内获取的所有资源也已关闭。这应该与从中获取资源的上下文配对,而不是使用全局状态。
根据您的客户使用的测试框架,您可以提供更好的服务。例如,JUnit5有一个基于注释的扩展工具,它允许您将上下文作为参数提供,并在每个测试执行后应用检查。 (但我没有太多使用它,所以我不会再说了什么。)
我会通过Factory methods
为这些对象提供实例,并且我可以控制它们的创建,并且我将使用Proxies
为消费者提供关闭对象的逻辑
interface Service<T> {
T execute();
void close();
}
class HeavyObject implements Service<SomeObject> {
SomeObject execute() {
// .. some logic here
}
private HeavyObject() {}
public static HeavyObject create() {
return new HeavyObjectProxy(new HeavyObject());
}
public void close() {
// .. the closing logic here
}
}
class HeavyObjectProxy extends HeavyObject {
public SomeObject execute() {
SomeObject value = super.execute();
super.close();
return value;
}
}