web-dev-qa-db-fra.com

Façon appropriée de diffuser à l'aide de ResponseEntity et de vous assurer que InputStream est fermé

Un de nos fichiers de fuites d'application gère et nous n'avons pas encore trouvé la cause de cela.

Dans le code, je peux voir plusieurs fonctions similaires à ceci:

public ResponseEntity<InputStreamResource> getFoo( ... ) {
    InputStream content = getContent(...)
    InputStreamResource isr = new InputStreamResource(content);
    return ResponseEntity.status(HttpServletResponse.SC_OK).body(isr);
}

(if vérifie et try/catch supprimés par souci de concision)

Je suis sûr que cette section provoque le problème car lorsque je teste ce code spécifique avec JMeter, je peux voir que getContent() échoue à cette étape:

is = Files.newInputStream(f.toPath());

Normalement, je ferme le InputStream mais parce que ce code court et simple, je ne peux pas fermer le flux avant return ou l'appel de body.

Lorsque j'exécute lsof (le code s'exécute sur Linux), je peux voir que des milliers de fichiers sont ouverts en mode lecture. Je suis donc sûr que ce problème est dû au fait que le flux ne se ferme pas.

Existe-t-il un code de bonnes pratiques que je devrais échanger?

11
Marged

vous pouvez essayer d'utiliser StreamingResponseBody

StreamingResponseBody

Un type de valeur de retour de méthode de contrôleur pour le traitement de demande asynchrone où l'application peut écrire directement dans la réponse OutputStream sans tenir le thread du conteneur Servlet.

Étant donné que vous travaillez sur un thread distinct, en écrivant directement dans la réponse, votre problème pour appeler close() avant que return soit résolu.

vous pouvez probablement commencer par l'exemple suivant

public ResponseEntity<StreamingResponseBody> export(...) throws FileNotFoundException {
    //...

    InputStream inputStream = new FileInputStream(new File("/path/to/example/file"));


    StreamingResponseBody responseBody = outputStream -> {

        int numberOfBytesToWrite;
        byte[] data = new byte[1024];
        while ((numberOfBytesToWrite = inputStream.read(data, 0, data.length)) != -1) {
            System.out.println("Writing some bytes..");
            outputStream.write(data, 0, numberOfBytesToWrite);
        }

        inputStream.close();
    };

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=generic_file_name.bin")
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(responseBody);
}

De plus, au lieu d'utiliser InputStream, vous pouvez également essayer d'utiliser Files (puisque Java 7)

vous n'avez donc pas à gérer InputStream

    File file = new File("/path/to/example/file");

    StreamingResponseBody responseBody = outputStream -> {
        Files.copy(file.toPath(), outputStream);
    };
4
ValerioMC

Vous pouvez refactoriser toutes vos méthodes de contrôleur qui lisent les fichiers locaux et définissez leur contenu comme le corps de la réponse HTTP:

Au lieu d'utiliser l'approche ResponseEntity, vous injectez le HttpServletResponse sous-jacent et copiez les octets du flux d'entrée renvoyé par votre méthode getContent(...) vers le flux de sortie du HttpServletResponse , par exemple en utilisant les méthodes utilitaires liées aux IO d'Apache CommonsIO ou de la bibliothèque Google Guava. Dans tous les cas, assurez-vous de fermer le flux d'entrée! Le code ci-dessous le fait implicitement en utilisant une instruction 'try-with-resources' qui ferme le flux d'entrée déclaré à la fin de l'instruction.

@RequestMapping(value="/foo", method=RequestMethod.GET)
public void getFoo(HttpServletResponse response) {
    // use Java7+ try-with-resources
    try (InputStream content = getContent(...)) {

        // if needed set content type and attachment header
        response.addHeader("Content-disposition", "attachment;filename=foo.txt");
        response.setContentType("txt/plain");

        // copy content stream to the HttpServletResponse's output stream
        IOUtils.copy(myStream, response.getOutputStream());

        response.flushBuffer();
    }
}

référence:

https://docs.Oracle.com/javase/7/docs/api/Java/io/InputStream.htmlhttps://docs.Oracle.com/javase/7/ docs/api/Java/lang/AutoCloseable.htmlhttps://docs.Oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.htmlhttps: // google.github.io/guava/releases/19.0/api/docs/com/google/common/io/ByteStreams.htmlhttps://commons.Apache.org/proper/commons-io/ javadocs/api-release/index.html

(surtout regardez les méthodes public static int copy(InputStream input, OutputStream output) throws IOException et public static int copyLarge(InputStream input, OutputStream output) throws IOException de la classe org.Apache.commons.io.IOUtils)

3

En supposant que vous utilisez Spring, votre méthode pourrait renvoyer un Resource et laisser Spring gérer le reste (y compris la fermeture du flux sous-jacent). Il y a peu d'implémentations de Resource sont disponibles dans Spring API ou bien vous devez implémenter la vôtre. En fin de compte, votre méthode deviendrait simple et voudrait quelque chose comme ci-dessous

public ResponseEntity<Resource> getFo0(...) {
    return new InputStreamResource(<Your input stream>);
}
2
Stackee007

Parce que ce InputStream provient essentiellement d'un simple fichier, un bon remplacement est ce code:

FileSystemResource fsr = new FileSystemResource(fileName);
return ResponseEntity.status(HttpServletResponse.SC_OK).body(fsr);

FileSystemResource peut prendre un Java.util.File, une Java.nio.file.Path ou même un String pointant vers le fichier correspondant.

0
Marged