I am authoring a java library. Some of the classes that are meant to be used by library users, hold native system resources (over JNI). I\'d like to ensure that the user \"dispo
This post does not directly answer your question but provides a different point of view.
One approach to make your clients consistently call close
is to free them from this responsibility.
Use template pattern.
You mentioned that you're working with TCP, so let's assume that you have a TcpConnection
class that has a close()
method.
Let's define TcpConnectionOperations
interface:
public interface TcpConnectionOperations {
T doWithConnection(TcpConnectionAction action);
}
and implement it:
public class TcpConnectionTemplate implements TcpConnectionOperations {
@Override
public T doWithConnection(TcpConnectionAction action) {
try (TcpConnection tcpConnection = getConnection()) {
return action.doWithConnection(tcpConnection);
}
}
}
TcpConnectionAction
is just a callback, nothing fancy.
public interface TcpConnectionAction {
T doWithConnection(TcpConnection tcpConnection);
}
TcpConnectionOperations
interface.For example:
String s = tcpConnectionOperations.doWithConnection(connection -> {
// do what we with with the connection
// returning to string for example
return connection.toString();
});
TcpConnection
TcpConnectionOperations
and mock TcpConnections
and make assertions against themThis approach may not work if the lifecycle of a resource is longer than action
. E.g. it is necessary for the client to keep the resource for a longer time.
Then you might want to dive deep in ReferenceQueue/Cleaner (since Java 9) and related API.
This pattern is widely used in Spring framework.
See for example:
This is some kind of pooling:
a pool is a collection of resources that are kept ready to use, rather than acquired on use and released
Some pools in Java:
When implementing a pool several questions are raised:
close
d?When the resource should be close
d?
Usually pools provide an explicit close
method (it may have a different name but the purpose is the same) which closes all the resources held.
How it can be shared across multiple threads?
It depends on a kind of the resource itself.
Usually you want to ensure that only one thread accesses one resource.
This can be done using some kind of locking
Note that code provided here is only for demonstration purposes It has awful performance and violates some OOP principles.
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 cache
= new HashMap<>();
private boolean closed;
public CachingTcpConnectionTemplate() {
System.out.println("Created new template");
}
@Override
public synchronized T doWithConnectionTo(IpAndPort ipAndPort, TcpConnectionAction 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 entry : cache.entrySet()) {
entry.getValue().close();
}
System.out.println("Template closed");
}
}
Tests infrastructure
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();
}
}
The ParameterResolver and AfterAllCallback are from JUnit.
@ReuseTemplate
is a custom annotation
ReuseTemplate.java
:
@Retention(RetentionPolicy.RUNTIME)
public @interface ReuseTemplate {
}
Finally test:
@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();
});
}
}
Running:
$ mvn test
Output:
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
The key observation here is that connections are reused (see "instance=
")
This is oversimplified example of what can be done. Sure, in the real world pooling connections is not so simple. The pool should not grow indefinitely, connections can be kept only for specific period of time and so on. Usually some problems are solved by having something in the background.
I don't see how to use
try-with-resources statement
in the context of tests (I'm usingJUnit5
withMockito
), in that the "resource" is not short-lived - it is part of the test fixture.
See Junit 5 User Guide. Extension model
Being diligent as always, I tried implementing
finalize()
and testing for closure there, but it turns outfinalize()
is not even called (Java10). This is also marked as deprecated and I'm sure this idea will be frowned upon.
You overrode finalize
so that it throws an exception but they are ignored.
See Object#finalize
If an uncaught exception is thrown by the finalize method, the exception is ignored and finalization of that object terminates.
The best you can do here is to log the resource leakage and close
the resource
To be clear, I want the application's tests (that use my library) to fail if they don't call
close()
on my objects.
How do application tests use your resource? Do they instantiate it using new
operator?
If yes then I think PowerMock can help you (but I'm not sure)
If you have hidden instantiation of the resource behind some kind of a factory then you can give the application tests some mock factory
If you're interested you can watch this talk. It's in Russian, but still may be helpful (part of my answer is based on this talk).