这篇文章并没有直接回答你的问题,但提供了一个不同的观点。
让你的客户端始终调用close
的方法之一是使他们摆脱这个责任。
如何做到这一点?
使用模板模式。
实现草图
您提到您正在使用TCP,因此让我们假设您有一个TcpConnection
类,它具有一个close()
方法。
让我们定义TcpConnectionOperations
接口:
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 -> {
return connection.toString();
});
优点
- 客户端不需要担心:
- 您可以控制创建连接:
- 在测试中,您可以提供模拟的
TcpConnectionOperations
和模拟的 TcpConnections
并对它们进行断言
缺点
如果资源的生命周期长于 action
,则此方法可能无法工作。例如,客户端需要更长时间地保留资源。
然后,您可能需要深入了解 ReferenceQueue
/Cleaner
(自 Java 9 起)和相关 API。
受 Spring 框架启发
该模式广泛用于 Spring 框架。
例如:
更新2/7/19
如何缓存/重用资源?
这是一种pooling:
池是一组资源,它们被准备好供使用,而不是在使用时获取并释放
Java中的一些池:
在实现池时会引发几个问题:
- 资源何时应该被关闭?
- 如何在多个线程之间共享资源?
资源何时应该被关闭?
通常,池提供了一个显式的close
方法(它可能有不同的名称,但目的是相同的),用于关闭所有持有的资源。
如何在多个线程之间共享资源?
这取决于资源本身的类型。
通常,您希望确保每个资源只有一个线程访问。
可以使用某种形式的锁定来实现这一点。
演示
请注意,此处提供的代码仅用于演示目的
它的性能很差,违反了一些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");
}
}
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用户指南。扩展模型
一如既往地认真,我尝试实现
finalize()
并在那里测试关闭,但事实证明
finalize()
甚至没有被调用(Java10)。这也被标记为过时的,我相信这个想法会遭到反对。
您覆盖了
finalize
,以便它抛出异常,但它们被忽略了。
请参见
Object#finalize
。
如果由finalize方法抛出未捕获的异常,则会忽略该异常,并终止该对象的最终处理。
在这里,你最好做的是记录资源泄漏并“关闭”资源。
引述:
要明确一点,我希望应用程序的测试(使用我的库)如果没有调用close()函数来关闭我的对象,则测试失败。
应用程序测试如何使用你的资源?他们是否使用new运算符实例化它?
如果是的话,我认为
PowerMock可以帮助你(但我不确定)。
如果你把资源的实例化隐藏在某种工厂之后,那么你可以给应用程序测试一些模拟工厂。
如果您感兴趣,可以观看这个
talk。虽然是用俄语演讲的,但仍可能有所帮助(我的回答部分就是基于这个演讲的)。
AutoClosable
的文档:“请注意,与Closeable
的close
方法不同,此close
方法不需要是幂等的。换句话说,调用此close
方法多次可能会产生某些可见的副作用,而Closeable.close
则要求在多次调用时不会产生任何效果。”- 您可以在第一次调用close
后抛出ResourceAlreadyClosedException
,然后指示用户在其测试中检查该异常。 - MTCosterfinalize
-现在有一个叫做Cleaner
的新东西,它比finalizers更受欢迎。正如API页面上的示例所示,它还可以与autocloseables很好地配合使用。但是,仍然不能保证它们何时甚至是否运行(除非您手动调用清理)。 - gustafc