在Java中,如何检查AutoCloseable.close()是否已被调用?

我正在编写一个java库。一些供库用户使用的类保存本机系统资源(通过 JNI)。我想确保用户“处置”这些对象,因为它们很重,并且在测试套件中它们可能会导致测试用例之间的泄漏(例如,我需要确保将处理)。为此,我使Java类实现了AutoCloseable,但这似乎还不够,或者我没有正确使用它:TearDown

  1. 我不明白如何在测试的上下文中使用语句(我使用),因为“资源”不是短暂的 - 它是测试夹具的一部分。try-with-resourcesJUnit5Mockito

  2. 一如既往地勤奋,我尝试在那里实现和测试闭包,但事实证明它甚至没有被称为(Java10)。这也被标记为已弃用,我相信这个想法会不受欢迎。finalize()finalize()

这是如何完成的?为了清楚起见,我希望应用程序的测试(使用我的库)失败,如果他们不在我的对象上调用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


答案 1

这篇文章没有直接回答你的问题,但提供了不同的观点。

让您的客户始终如一地打电话的一种方法是将他们从这种责任中解放出来。close

你怎么能做到呢?

使用模板模式。

草图实现

你提到你正在使用TCP,所以让我们假设你有一个具有方法的类。TcpConnectionclose()

让我们定义接口: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 -> {
  // do what we with with the connection
  // returning to string for example
  return connection.toString();
});

优点

  • 客户不必担心:
    • 获取TcpConnection
    • 关闭连接
  • 您可以控制创建连接:
    • 您可以缓存它们
    • 记录它们
    • 收集统计信息
    • 许多其他用例...
  • 在测试中,你可以提供模拟和模拟,并对它们做出断言TcpConnectionOperationsTcpConnections

缺点

如果资源的生命周期长于 ,则此方法可能不起作用。例如,客户端有必要将资源保留更长的时间。action

然后,您可能希望深入研究 ReferenceQueue/Cleaner(从 Java 9 开始)和相关 API。

灵感来自弹簧框架

这种模式在Spring框架中被广泛使用。

例如,请参阅:

更新 2019/2/7

如何缓存/重用资源?

这是某种池化

池是随时可供使用的资源的集合,而不是在使用时获得和释放

Java中的一些

实现池时,会提出几个问题:

  • 资源何时实际应为 d?close
  • 如何在多个线程之间共享资源?

何时应关闭资源 d?

通常,池提供显式关闭方法(它可能具有不同的名称,但目的是相同的),该方法关闭所有持有的资源。

如何在多个线程之间共享它?

它取决于资源本身的一种。

通常,您希望确保只有一个线程访问一个资源。

这可以使用某种锁定来完成

演示

请注意,此处提供的代码仅用于演示目的 它具有糟糕的性能并违反了某些 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();
  }
}

ParameterResolverAfterAllCallback 来自 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 statementJUnit5Mockito

请参阅 Junit 5 用户指南。扩展模型

一如既往地勤奋,我尝试在那里实现和测试闭包,但事实证明它甚至没有被称为(Java10)。这也被标记为已弃用,我相信这个想法会不受欢迎。finalize()finalize()

您覆盖,以便它引发异常,但它们被忽略。finalize

请参阅对象#finalize

如果 finalize 方法引发未捕获的异常,则忽略该异常并终止该对象的终结。

您在这里可以做的最好的事情就是记录资源泄漏和资源close

为了清楚起见,我希望应用程序的测试(使用我的库)在不调用我的对象时失败。close()

应用程序测试如何使用您的资源?他们是否使用运算符实例化它?如果是,那么我认为PowerMock可以帮助你(但我不确定)new

如果你在某种工厂后面隐藏了资源的实例化,那么你可以给应用程序测试一些模拟工厂。


如果你有兴趣,你可以观看这个演讲。它是俄语的,但仍然可能有帮助(我的部分答案是基于这个演讲)。


答案 2

如果我是你,我会做以下事情:

  • 在调用周围编写一个静态包装器,返回“重”对象
  • 创建 PhantomReference 集合以保存所有重对象,以便进行清理
  • 创建一个弱引用集合来保存所有重对象,以检查它们是否是GC'd(是否具有来自调用方的任何引用)
  • 在拆解时,我会检查包装器以查看哪些资源已被GC'd(在Phantom中有参考,但在弱资源中没有引用),并且我会检查它们是否已关闭或是否正确关闭。
  • 如果在提供资源时添加一些调试/调用方/堆栈跟踪信息,则更容易追溯泄漏的测试用例。

这也取决于你是否想在生产环境中使用此机制 - 也许值得将此功能添加到你的lib中,因为资源管理在生产环境中也是一个问题。在这种情况下,您不需要包装器,但您可以使用此功能扩展当前类。您可以使用后台线程进行定期检查,而不是拆解。

关于引用类型,我推荐此链接。建议将幻像引用用于资源清理。


推荐