在Java中,如何检查已经调用了AutoCloseable.close()方法?

29

我正在编写一个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");
    }
}

编辑2,赏金的结果 感谢所有回答的人,赏金的一半已经自动授予。我得出结论,在我的情况下,最好尝试涉及Cleaner的解决方案。然而,似乎虽然注册了清理操作,但它们没有被调用。我在这里提出了跟进问题here


5
如果资源没有被关闭,你的库的测试不需要失败。因为关闭操作明确地暴露给了用户,这是他们应该检查的事情。但是,你可能希望测试调用关闭操作实际上是否关闭了系统资源,在这种情况下,你可以在测试中显式地关闭资源。 - MTCoster
1
你的问题只是关于测试还是一般强制使用 #close 方法? - AdamSkywalker
@AdamSkywalker,这个问题适用于一般情况下使用我的API,但我会用测试来说明。如果不关闭应用程序,可能会发生一些不好的事情。举个例子,对于TCP连接,可以考虑SO_LINGER选项。在本地端,我有一些状态机需要进入某些最终状态(你可能会问,既然应用程序已经关闭了,那如果它们没有进入最终状态怎么办?其实不完全是这样,有时候你需要“关闭”以确保所有数据已在网络中传输)。 - haelix
3
根据AutoClosable的文档:“请注意,与Closeableclose方法不同,此close方法不需要是幂等的。换句话说,调用此close方法多次可能会产生某些可见的副作用,而Closeable.close则要求在多次调用时不会产生任何效果。”- 您可以在第一次调用close后抛出ResourceAlreadyClosedException,然后指示用户在其测试中检查该异常。 - MTCoster
1
关于finalize-现在有一个叫做Cleaner的新东西,它比finalizers更受欢迎。正如API页面上的示例所示,它还可以与autocloseables很好地配合使用。但是,仍然不能保证它们何时甚至是否运行(除非您手动调用清理)。 - gustafc
显示剩余5条评论
5个回答

18

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

让你的客户端始终调用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 框架

例如:

  • JdbcTemplate:提供了一个简单的方法来使用JDBC API执行SQL语句并处理结果集。
  • TransactionTemplate:提供了编程式事务管理的模板类,可以在Java中进行声明式事务管理。
  • JmsTemplate:提供了发送和接收消息的方法,是使用Java Message Service(JMS)实现异步通信的一种方式。
  • (还有很多其他的)

更新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();
  }
}

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用户指南。扩展模型 一如既往地认真,我尝试实现finalize()并在那里测试关闭,但事实证明finalize()甚至没有被调用(Java10)。这也被标记为过时的,我相信这个想法会遭到反对。
您覆盖了finalize,以便它抛出异常,但它们被忽略了。
请参见Object#finalize
如果由finalize方法抛出未捕获的异常,则会忽略该异常,并终止该对象的最终处理。
在这里,你最好做的是记录资源泄漏并“关闭”资源。
引述:
要明确一点,我希望应用程序的测试(使用我的库)如果没有调用close()函数来关闭我的对象,则测试失败。
应用程序测试如何使用你的资源?他们是否使用new运算符实例化它?
如果是的话,我认为PowerMock可以帮助你(但我不确定)。
如果你把资源的实例化隐藏在某种工厂之后,那么你可以给应用程序测试一些模拟工厂。
如果您感兴趣,可以观看这个talk。虽然是用俄语演讲的,但仍可能有所帮助(我的回答部分就是基于这个演讲的)。

如果资源应该在线程之间共享,或者应该与其他事务混合使用,您会推荐什么? - gaborsch
@gaborsch 我认为这取决于分享的资源类型...你是否有类似于某种连接池的场景?那么你可以查看一下,例如HikariCP如何管理连接。OkHttpClientConnectionPool也可以提供一些见解。如果你有处理并发写入/读取的东西,那么你将不得不处理ReadWriteLock。但是何时、如何以及哪个线程应该关闭资源取决于特定的用例... - Denis Zavedeev
你可以将它们缓存起来 - 不行,如果你将它们缓存起来,你就不知道什么时候应该 close() 它们了,我们又回到了问题的起点。当然,这个资源是长期存在的,问题中已经提到了这一点。 - haelix

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

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

关于引用类型,我推荐使用这个链接。建议使用PhantomReferences来进行资源清理。


1
我认为涉及Phantom或Weak的解决方案是正确的选择,基本上需要一种通知机制,以便我的库“知道”用户何时不再持有强引用。在那个时候,她应该关闭()资源,否则库会释放地狱。 - haelix
1
如果他们没有关闭(),在测试中您可以将该情况标记为失败,在生产中您可以默默地为他们关闭它,释放资源。 - gaborsch
1
“默默地为他们关闭它”- 在我的情况下,这不是一个选项。本机对象很敏感,需要从创建它的线程关闭,这可能是用户线程。希望这能更好地解释为什么用户必须确定性地关闭它,以及为什么整个讨论如此重要。 - haelix
我明白了。在这种情况下,我会考虑两个选项:1)创建一个管理线程,在请求时创建和关闭对象,即使在生产环境中也能正常工作;2)跟踪线程和创建 individual.objects 的堆栈跟踪,并在检测到故障时报告它们。这有助于错误检测。 - gaborsch

1
如果您对测试中的一致性感兴趣,只需将标有@AfterClass注释的destroy()方法添加到测试类中,并在其中关闭所有先前分配的资源。
如果您希望采用一种方法来保护资源不被关闭,您可以提供一种不会明确暴露资源给用户的方式。例如,您的代码可以控制资源的生命周期,并仅接受用户的Consumer<T>
如果您无法这样做,但仍想确保即使用户使用不正确也能关闭资源,您将不得不做一些棘手的事情。您可以将资源拆分为sharedPtr和资源本身。然后将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();
        });
    }
}

1

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

第一件要做的事是使客户端对资源处理变得容易。使用“执行环绕”惯用法。

据我所知,在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具有基于注释的扩展功能,允许您将上下文作为参数提供,并在每次执行测试后应用检查。(但我没有多少使用经验,所以我不会再说什么了。)


通常情况下,如果您可以可靠地测试资源是否已关闭,则可以自行关闭它 - 这是错误的。在我的情况下,这不是一个选项。本机对象很敏感,需要从创建它的线程中关闭,这可能是用户线程。希望这能更清楚地说明为什么用户必须确定性地关闭该对象以及整个讨论的原因。 - haelix

0
我会通过工厂方法为这些对象提供实例,并且通过这种方式我可以控制它们的创建,然后我将使用代理来提供给消费者,代理会执行关闭对象的逻辑。
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()资源。 - haelix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接