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

hae*_*lix 26 java dispose finalize try-with-resources autocloseable

我正在创作一个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");
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑2,赏金的结果感谢所有的回复,一半的赏金被自动授予.我的结论是,对于我的情况,最好尝试解决方案Cleaner.但是看起来,虽然已经注册,但是不会调用清理操作.我在这里问了一个跟进问题.

cac*_*co3 15

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

让您的客户始终致电的一种方法是让close他们免除此责任.

你怎么能这样做?

使用模板模式.

草图实施

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

我们来定义TcpConnectionOperations界面:

public interface TcpConnectionOperations {
  <T> T doWithConnection(TcpConnectionAction<T> action);
}
Run Code Online (Sandbox Code Playgroud)

并实现它:

public class TcpConnectionTemplate implements TcpConnectionOperations {
  @Override
  public <T> T doWithConnection(TcpConnectionAction<T> action) {
    try (TcpConnection tcpConnection = getConnection()) {
      return action.doWithConnection(tcpConnection);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

TcpConnectionAction 只是一个回调,没什么特别的.

public interface TcpConnectionAction<T> {
  T doWithConnection(TcpConnection tcpConnection);
}
Run Code Online (Sandbox Code Playgroud)

现在应该如何消费图书馆?

  • 它必须通过TcpConnectionOperations接口使用.
  • 消费者提供行动

例如:

String s = tcpConnectionOperations.doWithConnection(connection -> {
  // do what we with with the connection
  // returning to string for example
  return connection.toString();
});
Run Code Online (Sandbox Code Playgroud)

优点

  • 客户不必担心:
    • 得到一个 TcpConnection
    • 关闭连接
  • 您可以控制创建连接:
    • 你可以缓存它们
    • 记录它们
    • 收集统计数据
    • 许多其他用例......
  • 在测试中,您可以提供模拟TcpConnectionOperations和模拟TcpConnections,并对它们进行断言

缺点

如果资源的生命周期长于,则此方法可能无效action.例如,客户端需要将资源保留更长时间.

然后你可能想深入研究ReferenceQueue/ Cleaner(自Java 9开始)和相关的API.

受Spring框架的启发

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

参见例如:

更新2/7/19

如何缓存/重用资源?

这是某种汇集:

池是一组资源,可以随时使用,而不是在使用和获取时获取

Java中的一些:

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

  • 当资源实际应该是closed?
  • 如何在多个线程之间共享资源?

当资源应该是closed?

通常,池提供了一个显式close方法(它可能有不同的名称,但目的是相同的),它会关闭所有持有的资源.

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

它取决于资源本身的种类.

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

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

演示

请注意,此处提供的代码仅用于演示目的.它具有糟糕的性能并违反了一些OOP原则.

IpAndPort.java

@Value
public class IpAndPort {
  InetAddress address;
  int port;
}
Run Code Online (Sandbox Code Playgroud)

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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");
  }
}
Run Code Online (Sandbox Code Playgroud) 测试基础设施

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();
  }
}
Run Code Online (Sandbox Code Playgroud)

ParameterResolverAfterAllCallback来自JUnit的.

@ReuseTemplate 是一个自定义注释

ReuseTemplate.java:

@Retention(RetentionPolicy.RUNTIME)
public @interface ReuseTemplate {
}
Run Code Online (Sandbox Code Playgroud)

最后测试:

@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();
    });
  }
}
Run Code Online (Sandbox Code Playgroud)

运行:

$ mvn test
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

这里的关键观察是连接被重用(参见" instance=")

这是可以做什么的过于简单的例子.当然,在现实世界中,联系并不是那么简单.池不应无限增长,连接只能保持特定的时间段等等.通常在后台有一些问题可以解决一些问题.

回到问题

我不知道如何使用try-with-resources statement在测试的情况下(我使用JUnit5Mockito),在"资源"是不是短命的-这是测试夹具的一部分.

请参见Junit 5用户指南.扩展模型

像往常一样勤奋,我尝试finalize()在那里实现和测试闭包,但事实finalize()证明甚至没有调用(Java10).这也被标记为已弃用,我确信这个想法不会受到诟病.

你覆盖,finalize以便它抛出异常但它们被忽略.

看到 Object#finalize

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

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

要清楚,如果他们不调用close()我的对象,我希望应用程序的测试(使用我的库)失败.

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

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


如果你有兴趣,你可以看这个演讲.这是俄语,但仍然可能有用(我的答案的一部分是基于这个演讲).


gab*_*sch 6

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

  • 在返回"重"对象的调用周围写一个静态包装器
  • 创建一个PhantomReferences集合来保存所有重物,以便进行清理
  • 创建一个WeakReferences集合来保存所有重物,检查它们是否为GC(是否有来自调用者的任何引用)
  • 在拆卸我会检查包装看资源已GC'd什么(有幻影参考,但不能在弱),我会检查自己是否已被关闭或也不正常.
  • 如果在提供资源时添加一些debug/caller/stacktrace信息,则更容易追溯泄漏的测试用例.

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

关于参考类型,我建议这个链接.建议将PhantomReferences用于资源清理.


dmi*_*ony 1

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

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

如果您不能做到这一点,但仍然希望确保资源将被关闭,即使用户没有正确使用它,您将不得不做一些棘手的事情。您可以将您的资源sharedPtr分开resource。然后暴露sharedPtr给用户并将其放入包装到的一些内部存储中WeakReference。因此,您将能够捕获 GC 删除sharedPtrclose()调用resource. 请注意,resource不得暴露给用户。我准备了一个例子,虽然不太准确,但希望它能说明这个想法:

public interface Resource extends AutoCloseable {

    public int jniCall();
}
Run Code Online (Sandbox Code Playgroud)
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");
    }
}
Run Code Online (Sandbox Code Playgroud)
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();
    }
}
Run Code Online (Sandbox Code Playgroud)
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();
        });
    }
}
Run Code Online (Sandbox Code Playgroud)