29 votes

En Java, comment vérifier que AutoCloseable.close() a été appelé ?

Je suis en train de créer une bibliothèque java. Certaines des classes destinées à être utilisées par les utilisateurs de la bibliothèque contiennent des ressources système natives (via JNI). J'aimerais m'assurer que l'utilisateur "dispose" de ces objets, car ils sont lourds, et dans une suite de tests, ils peuvent provoquer des fuites entre les cas de test (par exemple, je dois m'assurer que TearDown disposera). À cette fin, j'ai fait en sorte que les classes Java implémentent AutoCloseable, mais cela ne semble pas suffire, ou je ne l'utilise pas correctement :

  1. Je ne vois pas comment utiliser try-with-resources dans le contexte des tests (j'utilise l'instruction JUnit5 con Mockito ), en ce sens que la "ressource" n'est pas éphémère - elle fait partie du dispositif d'essai.

  2. É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.

Comment cela se fait-il ? 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.


Editar: en ajoutant un peu de code si cela peut aider. Ce n'est pas grand-chose, mais c'est ce que j'essaie de faire.

@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, résultat de la prime Merci à tous d'avoir répondu, la moitié de la prime a été automatiquement attribuée. J'ai conclu que pour mon cas, il serait préférable d'essayer la solution impliquant Cleaner . Cependant, il semble que les actions de nettoyage, bien qu'enregistrées, ne sont pas invoquées. J'ai posé une question de suivi aquí .

17voto

caco3 Points 2353

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 :
    • obtenir un TcpConnection
    • fermer la connexion
  • C'est vous qui contrôlez la création de connexions :
    • vous pouvez les mettre en cache
    • les enregistrer
    • 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é).

6voto

GaborSch Points 6587

Si j'étais vous, je ferais ce qui suit :

  • Écrivez une enveloppe statique autour de vos appels qui renvoie des objets "lourds".
  • Créez une collection de PhantomReferences pour contenir tous vos objets lourds, à des fins de nettoyage
  • Créez une collection de Références faibles pour contenir tous vos objets lourds, pour vérifier s'ils sont GC'd ou non (ont une référence de l'appelant ou non).
  • Lors du démontage, je vérifierais le wrapper pour voir quelles ressources ont été GC'd (ont une référence dans le Phantom, mais pas dans le Weak), et je vérifierais si elles ont été fermées ou ni correctement.
  • Si vous ajoutez des informations de débogage/appelant/stacktrace pendant que vous servez la ressource, il sera plus facile de retrouver le cas de test qui fuit.

Cela dépend aussi si vous voulez utiliser ce mécanisme en production ou non - il est peut-être utile d'ajouter cette fonctionnalité à votre librairie, car la gestion des ressources sera aussi un problème dans un environnement de production. Dans ce cas, vous n'avez pas besoin d'un wrapper, mais vous pouvez étendre vos classes actuelles avec cette fonctionnalité. Au lieu d'un démontage, vous pouvez utiliser un thread d'arrière-plan pour des vérifications régulières.

En ce qui concerne les types de référence, je recommande ce lien . Il est recommandé d'utiliser les PhantomReferences pour le nettoyage des ressources.

1voto

dmitrievanthony Points 738

Si vous êtes intéressé par la cohérence des tests, il suffit d'ajouter la méthode destroy() marqué par @AfterClass dans la classe de test et fermer toutes les ressources précédemment allouées dans celle-ci.

Si vous êtes intéressé par une approche qui vous permet de protéger la ressource contre la non-fermeture, vous pouvez fournir un moyen qui n'expose pas explicitement la ressource à l'utilisateur. Par exemple, votre code pourrait contrôler le cycle de vie de la ressource et n'accepter que les éléments suivants Consumer<T> de l'utilisateur.

Si vous ne pouvez pas faire cela, mais que vous voulez quand même être sûr que la ressource sera fermée même si l'utilisateur ne l'utilise pas correctement, vous devrez faire plusieurs choses délicates. Vous pourriez diviser votre ressource sur sharedPtr y resource même. Ensuite, exposez sharedPtr à l'utilisateur et le mettre dans un stockage interne emballé dans WeakReference . Ainsi, vous serez en mesure d'attraper le moment où le GC enlèvera l'eau. sharedPtr et appeler close() sur le resource . Sachez que resource ne doit pas être exposé à l'utilisateur. J'ai préparé un exemple, il n'est pas très précis, mais j'espère qu'il montre l'idée :

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

1voto

En général, si vous pouviez tester de manière fiable si une ressource a été fermée, vous pourriez simplement la fermer vous-même.

La première chose à faire est de rendre la manipulation de la ressource facile pour le client. Utilisez l'idiome Execute Around.

Pour autant que je sache, la seule utilisation de execute around pour la gestion des ressources dans la bibliothèque Java est java.security.AccessController.doPrivileged et qui est spéciale (la ressource est un cadre de pile magique, que vous vraiment ne veulent pas laisser ouvert). Je crois que Spring dispose depuis longtemps d'une bibliothèque JDBC indispensable à cet effet. J'ai certainement utilisé execute-around (je ne savais pas que cela s'appelait ainsi à l'époque) pour JDBC peu de temps après que Java 1.1 l'ait rendu vaguement pratique.

Le code de la bibliothèque devrait ressembler à quelque chose comme :

@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);
        }
    }

(Il faut mettre les déclarations de paramètres de type au bon endroit).

L'utilisation côté client ressemble à quelque chose comme :

MyType myResult = yourContext.doContext(resource -> {
    ...blah...;
    return ...thing...;
});

Retour aux tests. Comment faire pour qu'il soit facile de tester même si le testeur exfiltre la ressource de l'exécution autour ou si un autre mécanisme est disponible ?

La réponse évidente est que vous fournissez la solution "execute around" au test. Vous devrez fournir une API utilisant la fonction "execute around" pour vérifier que toutes les ressources acquises dans la portée ont également été fermées. Cela doit être associé au contexte dans lequel la ressource est acquise plutôt que d'utiliser l'état global.

En fonction du cadre de test utilisé par vos clients, vous pourrez peut-être proposer quelque chose de mieux. JUnit5, par exemple, dispose d'une extension basée sur les annotations qui vous permet de fournir le contexte comme argument et d'appliquer des vérifications après l'exécution de chaque test. (Mais je ne l'ai pas beaucoup utilisé, donc je ne vais pas en dire plus).

0voto

mehdi maick Points 325

Je fournirais des instances à ces objets par le biais de Factory methods et avec ça, j'ai le contrôle sur leur création, et je nourrirai les consommateurs de Proxies qui fait la logique de fermeture du site l'objet

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;
  }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X