Ce billet ne répond pas directement à votre question mais apporte un point de vue différent.
Une approche pour que vos clients appellent régulièrement close
est de les libérer de cette responsabilité.
Comment pouvez-vous le faire ?
Utilisez un modèle de patron.
Mise en œuvre de l'esquisse
Vous avez mentionné que vous travaillez avec TCP, alors supposons que vous avez un TcpConnection
qui a une close()
méthode.
Définissons TcpConnectionOperations
interface :
public interface TcpConnectionOperations {
<T> T doWithConnection(TcpConnectionAction<T> action);
}
et la mettre en œuvre :
public class TcpConnectionTemplate implements TcpConnectionOperations {
@Override
public <T> T doWithConnection(TcpConnectionAction<T> action) {
try (TcpConnection tcpConnection = getConnection()) {
return action.doWithConnection(tcpConnection);
}
}
}
TcpConnectionAction
est juste un callback, rien d'extraordinaire.
public interface TcpConnectionAction<T> {
T doWithConnection(TcpConnection tcpConnection);
}
Comment la bibliothèque doit-elle être consommée maintenant ?
- Il doit être consommé seulement par le biais de
TcpConnectionOperations
interface.
- Actions d'approvisionnement des consommateurs
Par exemple :
String s = tcpConnectionOperations.doWithConnection(connection -> {
// do what we with with the connection
// returning to string for example
return connection.toString();
});
Pour
- Les clients n'ont pas à s'inquiéter :
-
-
- C'est vous qui contrôlez la création de connexions :
-
- vous pouvez les mettre en cache
-
-
- recueillir des statistiques
-
- de nombreux autres cas d'utilisation...
- Dans les tests, vous pouvez fournir des simulations
TcpConnectionOperations
et se moquer TcpConnections
et faire des affirmations contre eux
Cons
Cette approche peut s'avérer inefficace si le cycle de vie d'une ressource est plus long que la durée de vie de la ressource. action
. Par exemple, il est nécessaire que le client conserve la ressource pendant une période plus longue.
Alors vous voudrez peut-être vous plonger dans ReferenceQueue
/ Cleaner
(depuis Java 9) et l'API correspondante.
Inspiré par le framework Spring
Ce modèle est largement utilisé dans Cadre de travail de Spring .
Voir par exemple :
Mise à jour 2/7/19
Comment puis-je mettre en cache/réutiliser la ressource ?
C'est une sorte de mise en commun :
un pool est une collection de ressources qui sont maintenues prêtes à l'emploi, plutôt que d'être acquises à l'utilisation et libérées.
Quelques piscines en Java :
La mise en place d'une piscine soulève plusieurs questions :
- Lorsque la ressource doit effectivement être
close
d ?
- Comment la ressource doit-elle être partagée entre plusieurs threads ?
Quand la ressource doit être close
d ?
Habituellement, les pools fournissent un close
(elle peut avoir un nom différent mais le but est le même) qui ferme toutes les ressources détenues.
Comment peut-on le partager sur plusieurs fils ?
Cela dépend de la nature de la ressource elle-même.
En général, vous voulez vous assurer qu'un seul thread accède à une ressource.
Cela peut être fait en utilisant une sorte de verrouillage
Démo
Notez que le code fourni ici est uniquement destiné à des fins de démonstration. Il est très peu performant et viole certains principes de la POO.
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");
}
}
Tests de l'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();
}
}
Le site ParameterResolver
y AfterAllCallback
sont issus de JUnit.
@ReuseTemplate
est une annotation personnalisée
ReuseTemplate.java
:
@Retention(RetentionPolicy.RUNTIME)
public @interface ReuseTemplate {
}
Enfin, testez :
@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();
});
}
}
En cours d'exécution :
$ mvn test
Sortie :
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
L'observation clé ici est que les connexions sont réutilisées (voir " instance=
")
Il s'agit d'un exemple très simplifié de ce qui peut être fait. Bien sûr, dans le monde réel, la mise en commun des connexions n'est pas si simple. Le pool ne doit pas croître indéfiniment, les connexions ne peuvent être conservées que pendant une période déterminée, etc. En général, certains problèmes sont résolus en ayant quelque chose en arrière-plan.
Revenir à la question
Je ne vois pas comment utiliser try-with-resources statement
dans le contexte des tests (j'utilise JUnit5
con Mockito
), en ce sens que la "ressource" n'est pas éphémère - elle fait partie du dispositif d'essai.
Ver Guide d'utilisation de Junit 5. Modèle d'extension
Étant diligent comme toujours, j'ai essayé de mettre en œuvre finalize()
et de tester la fermeture là-bas, mais il s'avère que finalize()
n'est même pas appelé (Java10). Il est également marqué comme déprécié et je suis sûr que cette idée sera désapprouvée.
Vous avez passé outre finalize
de façon à ce qu'une exception soit levée mais qu'ils soient ignorés.
Ver [Object#finalize
](https://docs.oracle.com/javase/10/docs/api/java/lang/Object.html#finalize())
Si une exception non attrapée est levée par la méthode finalize, l'exception est ignorée et la finalisation de cet objet se termine.
Le mieux que vous puissiez faire ici est d'enregistrer la fuite de ressources et close
la ressource
Pour être clair, je veux que les tests de l'application (qui utilisent ma bibliothèque) échouent s'ils n'appellent pas close()
sur mes objets.
Comment les tests d'application utilisent-ils votre ressource ? L'instancient-ils en utilisant new
opérateur ? Si oui, je pense que PowerMock peut vous aider (mais je ne suis pas sûr)
Si vous avez caché l'instanciation de la ressource derrière une sorte de fabrique, vous pouvez donner aux tests d'application une fabrique fantaisie.
Si vous êtes intéressé, vous pouvez regarder ceci parler . Il est en russe, mais peut néanmoins être utile (une partie de ma réponse est basée sur cet exposé).