【问题标题】:In Java, how to check that AutoCloseable.close() has been called?在 Java 中,如何检查 AutoCloseable.close() 是否已被调用?
【发布时间】:2019-06-23 00:37:12
【问题描述】:

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

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

  2. 一如既往地勤奋,我尝试实现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");
    }
}

Edit2,赏金结果感谢大家的回复,一半的赏金自动发放。我得出结论,对于我的情况,最好尝试涉及Cleaner 的解决方案。然而看起来,清洁动作虽然已注册,但并未被调用。我问了一个后续问题here

【问题讨论】:

  • 如果资源没有关闭,你的库的测试不需要失败。由于该关闭操作明确地向用户公开,这是 他们的 测试应该检查的内容。但是,您可能希望测试调用关闭操作实际上会关闭系统资源,但在这种情况下,您可以在测试中显式关闭资源
  • 您的问题只是关于测试还是关于强制使用 #close 方法?
  • 听起来您想通过 应用程序的 测试来测试它,而不是通过库自己的测试。在这种情况下,您可以很好地模拟 AutoCloseable 对象,并检查模拟的 close 方法是否在您期望的时候被调用。有很多方法可以模拟它,Mockito、JMock...
  • From the docs on AutoClosable: “注意,与Closeableclose方法不同,这个close方法不需要是幂等的。换句话说,多次调用此close 方法可能会产生一些可见的副作用,这与Closeable.close 不同,如果多次调用则必须无效。” - 你可以抛出ResourceAlreadyClosedException在第一次调用close 之后,然后指示用户在他们的测试中检查该异常
  • @haelix 关于finalize- 有一个新的东西叫做Cleaner,现在它比终结者更受欢迎。它还可以很好地与 autocloseables 配合使用,如 API 页面上的示例所示。但是,仍然无法保证它们何时甚至是否会运行(除非您手动调用清理)。

标签: java dispose try-with-resources finalize autocloseable


【解决方案1】:

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

让您的客户持续致电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 -> {
  // 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 框架启发

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

参见示例:

2/7/19 更新

如何缓存/重用资源?

这是某种pooling

池是随时可用的资源集合,而不是在使用时获取并释放

Java 中的一些

在实施池时会提出几个问题:

  • 什么时候资源实际上应该是closed?
  • 如何在多个线程之间共享资源?

什么时候资源应该是closed?

通常池提供一个显式的 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();
  }
}

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 statement(我使用JUnit5Mockito),因为“资源”不是短暂的——它是测试夹具。

Junit 5 User Guide. Extension model

一如既往地勤奋,我尝试实现finalize() 并在那里测试闭包,但结果证明finalize() 甚至没有被调用(Java10)。这也被标记为已弃用,我相信这个想法会被反对。

您覆盖了finalize,因此它会引发异常,但它们会被忽略。

Object#finalize

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

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

明确地说,如果应用程序的测试(使用我的库)没有在我的对象上调用 close(),我希望它们失败。

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

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


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

【讨论】:

  • 如果资源应该在线程之间共享,或者应该与其他事务混合,您会建议什么?
  • @gaborsch 我认为这取决于共享的资源类型...您是否有类似于某种连接池的场景?然后您可以检查例如 HikariCP 如何管理连接。 OkHttpClientConnectionPool 也可以提供一些见解。如果你有处理并发写/读的事情,那么你将不得不处理ReadWriteLock。但是何时、如何以及哪个线程应该关闭资源取决于特定的用例......
  • 你可以缓存它们 - 不,你不能,如果你缓存它们,你不知道什么时候去close()它们,我们又回到了问题上.当然资源是长期存在的,问题提到了这一点。
【解决方案2】:

如果我是你,我会这样做:

  • 围绕返回“重”对象的调用编写静态包装器
  • 创建一个 PhantomReferences 集合来存放所有重物,以便清理
  • 创建一个 WeakReferences 集合来保存所有重物,检查它们是否被 GC(是否有来自调用者的任何引用)
  • 在拆解时,我会检查包装器以查看哪些资源已被 GC(在 Phantom 中有引用,但在 Weak 中没有),我会检查它们是否已关闭或未正确关闭。
  • 如果您在提供资源时添加一些调试/调用者/堆栈跟踪信息,将更容易追溯泄漏的测试用例。

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

关于引用类型,我推荐this link。建议将 PhantomReferences 用于资源清理。

【讨论】:

  • 我认为涉及 Phantom 或 Weak 的解决方案是可行的方法,基本上是某种通知机制,因此我的 lib 知道用户何时不再持有 Strong ref。 到那时,她应该关闭()资源,否则图书馆将释放地狱。
  • "应该已经关闭()" - 如果他们没有,在测试中你可以标记那个案例失败,在生产中你可以默默地为他们关闭它,释放资源。
  • 为他们默默地关闭它 - 这不是我的选择。本机对象是敏感的,需要从创建它的线程中关闭,该线程可能是用户线程。希望这能更清楚地说明为什么用户真的必须确定性地关闭这个东西,以及为什么整个讨论
  • 我明白了。在这种情况下,我会考虑两个选项:1)创建一个管理器线程,根据请求创建和关闭对象,即使在 prod 环境中也可以工作,2)跟踪创建 individual.objects 的线程和堆栈跟踪,并报告它们当检测到故障时。这将有助于错误检测。
【解决方案3】:

如果您对测试的一致性感兴趣,只需将带有@AfterClass 注释的方法destroy() 添加到测试类中,并关闭其中所有先前分配的资源。

如果您对允许您保护资源不被关闭的方法感兴趣,您可以提供一种不向用户显式公开资源的方法。例如,您的代码可以控制资源生命周期并仅接受来自用户的Consumer&lt;T&gt;

如果你不能这样做,但仍然想确保即使用户没有正确使用资源也会关闭它,你将不得不做一些棘手的事情。您可以在 sharedPtrresource 上拆分您的资源。然后将sharedPtr 暴露给用户并将其放入包裹在WeakReference 中的一些内部存储中。因此,您将能够捕捉到 GC 删除 sharedPtr 并在 resource 上调用 close() 的时刻。请注意,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();
        });
    }
}

【讨论】:

    【解决方案4】:

    一般来说,如果您可以可靠地测试资源是否已关闭,您可以自己关闭它。

    首先要做的是让客户端更容易处理资源。使用 Execute Around 习语。

    据我所知,在 Java 库中执行资源处理的唯一用途是 java.security.AccessController.doPrivileged,这很特别(资源是一个神奇的堆栈框架,您真的不这样做'不想打开)。我相信 Spring 长期以来一直为此提供急需的 JDBC 库。在 Java 1.1 模糊实用后不久,我肯定在使用 JDBC 执行(当时不知道它被称为)。

    库代码应该类似于:

    @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 有一个基于注释的扩展工具,它允许您提供上下文作为参数,并在每个测试执行后应用检查。 (不过我用的不多,就不多说了。)

    【讨论】:

    • 一般来说,如果你能可靠地测试一个资源是否已经关闭,你可以自己关闭它 - false。在我的情况下,这不是一个选择。本机对象是敏感的,需要从创建它的线程中关闭,该线程可能是用户线程。希望这能更清楚地说明为什么用户真的必须确定性地关闭这个东西,以及为什么整个讨论
    【解决方案5】:

    我将通过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;
      }
    }
    

    【讨论】:

    • 我不想在每次操作后close()资源。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多