web-dev-qa-db-fra.com

Utilisation de ServletOutputStream pour écrire de très gros fichiers dans un servlet Java sans problèmes de mémoire

J'utilise IBM Websphere Application Server v6 et Java 1.4 et j'essaie d'écrire des fichiers CSV volumineux sur la ServletOutputStream pour qu'un utilisateur puisse les télécharger. Les fichiers vont de 50 à 750 Mo pour le moment.

Les petits fichiers ne causent pas trop de problèmes, mais avec les plus gros fichiers, il apparaît qu’ils sont en train d’être écrits dans le tas, ce qui provoque une erreur OutOfMemory et fait tomber le serveur entier.

Ces fichiers ne peuvent être distribués qu'aux utilisateurs authentifiés via HTTPS. C'est pourquoi je les sers via un servlet au lieu de les coller dans Apache.

Le code que j'utilise est (des peluches ont été supprimées):

    resp.setHeader("Content-length", "" + fileLength);
    resp.setContentType("application/vnd.ms-Excel");
    resp.setHeader("Content-Disposition","attachment; filename=\"export.csv\"");

    FileInputStream inputStream = null;

    try
    {
        inputStream = new FileInputStream(path);
        byte[] buffer = new byte[1024];
        int bytesRead = 0;

        do
        {
            bytesRead = inputStream.read(buffer, offset, buffer.length);
            resp.getOutputStream().write(buffer, 0, bytesRead);
        }
        while (bytesRead == buffer.length);

        resp.getOutputStream().flush();
    }
    finally
    {
        if(inputStream != null)
            inputStream.close();
    }

La FileInputStream ne semble pas poser de problème, car si j'écris dans un autre fichier ou si je supprime simplement l'écriture, l'utilisation de la mémoire ne semble pas poser de problème.

Ce que je pense, c'est que la resp.getOutputStream().write est stockée en mémoire jusqu'à ce que les données puissent être envoyées au client. Il est donc possible que le fichier entier soit lu et stocké dans la resp.getOutputStream(), ce qui provoque des problèmes de mémoire et un blocage!

J'ai essayé de mettre en mémoire tampon ces flux et d'essayer également d'utiliser les chaînes de Java.nio, aucune de ces méthodes ne semblant faire une petite différence dans mes problèmes de mémoire. J'ai également vidé la OutputStream une fois par itération de la boucle et après la boucle, ce qui n'a pas aidé.

37
Martin

Le conteneur de servlet décent moyen lui-même vide le flux par défaut tous les ~ 2 Ko. Vous ne devriez vraiment pas avoir à appeler explicitement flush() sur la OutputStream de la HttpServletResponse à intervalles réguliers lors de la diffusion séquentielle de données à partir de la même source. Par exemple, dans Tomcat (et Websphere!), Il est possible de configurer l'attribut bufferSize du connecteur HTTP. 

Le servletcontainer décent moyen diffuse également simplement les données dans morceaux si la longueur du contenu est inconnue au préalable (conformément à la spécification d'API Servlet !) Et si le client prend en charge HTTP 1.1.

Les symptômes du problème indiquent au moins que servletcontainer met en mémoire tampon la totalité du flux en mémoire avant le vidage. Cela peut signifier que l'en-tête de longueur du contenu n'est pas défini et/ou le servletcontainer ne prend pas en charge le codage par blocs et/ou que le côté client ne prend pas en charge le codage par blocs (c'est-à-dire qu'il utilise HTTP 1.0).

Pour corriger l'un ou l'autre, il suffit de définir la longueur du contenu à l'avance:

response.setHeader("Content-Length", String.valueOf(new File(path).length()));
41
BalusC
  1. La classe de Kevin devrait fermer le champ m_out si elle n'est pas nulle dans l'opérateur close (), nous ne voulons pas laisser filtrer des objets, n'est-ce pas?

  2. En plus de l'opérateur ServletOutputStream.flush(), l'opération HttpServletResponse.flushBuffer() peut également vider les mémoires tampons. Cependant, il semble que la mise en œuvre de ces opérations ait un impact spécifique ou que la prise en charge de la longueur du contenu http interfère avec un détail spécifique à la mise en œuvre. Rappelez-vous que la spécification de la longueur du contenu est une option sur HTTP 1.0; les choses doivent donc simplement être écoulées si vous les videz. Mais je ne vois pas ça

1
SteveL

Donc, en suivant votre scénario, ne devriez-vous pas être entré dans cette boucle while (à chaque itération), au lieu de l’extraire? Je voudrais essayer cela, avec un tampon un peu plus grand cependant.

1
Kostas

flush fonctionne-t-il sur le flux de sortie?.

Vraiment, je voulais dire que vous devriez utiliser la forme d'écriture à trois arguments car le tampon n'est pas nécessairement entièrement lu (particulièrement à la fin du fichier (!)). De plus, un essai/enfin serait approprié sauf si vous voulez que votre serveur meure inopinément.

1

La condition tant que ne fonctionne pas, vous devez vérifier le -1 avant de l’utiliser. Et s'il vous plaît utilisez une variable temporaire pour le flux de sortie, son plus agréable à lire et il sécurise l'appelant getOutputStream () de manière lisible.

OutputStream outStream = resp.getOutputStream();
while(true) {
    int bytesRead = inputStream.read(buffer);
    if (bytesRead < 0)
      break;
    outStream.write(buffer, 0, bytesRead);
}
inputStream.close();
out.close();
1
eckes

Je ne sais pas non plus si flush() sur ServletOutputStream fonctionne dans ce cas, mais ServletResponse.flushBuffer() devrait envoyer la réponse au client (au moins conformément à la spécification de servlet 2.3).

ServletResponse.setBufferSize() semble également prometteur.

1
david a.

J'ai utilisé une classe qui encapsule le flux de sortie pour le rendre réutilisable dans d'autres contextes. Cela a bien fonctionné pour moi d’accéder plus rapidement aux données dans le navigateur, mais je n’ai pas examiné les conséquences pour la mémoire. (pardonnez mon nom de variable m_ obsolète)

import Java.io.IOException;
import Java.io.OutputStream;

public class AutoFlushOutputStream extends OutputStream {

    protected long m_count = 0;
    protected long m_limit = 4096; 
    protected OutputStream m_out;

    public AutoFlushOutputStream(OutputStream out) {
        m_out = out;
    }

    public AutoFlushOutputStream(OutputStream out, long limit) {
        m_out = out;
        m_limit = limit;
    }

    public void write(int b) throws IOException {

        if (m_out != null) {
            m_out.write(b);
            m_count++;
            if (m_limit > 0 && m_count >= m_limit) {
                m_out.flush();
                m_count = 0;
            }
        }
    }
}
1
Kevin Hakanson

votre code a une boucle infinie.

do
{
    bytesRead = inputStream.read(buffer, offset, buffer.length);
    resp.getOutputStream().write(buffer, 0, bytesRead);
}
while (bytesRead == buffer.length);

offset a la même valeur que dans la boucle, donc si initialement offset = 0 , il le restera à chaque itération, ce qui provoquera une boucle infinie et conduira à une erreur de MOO.

0
rooparam

indépendante de vos problèmes de mémoire, la boucle while devrait être:

while(bytesRead > 0);
0
james