Utiliser 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 de gros fichiers CSV à ServletOutputStream pour un utilisateur à télécharger. Les fichiers vont de 50 à 750MO pour le moment.

les petits fichiers ne causent pas trop de problèmes, mais avec les gros fichiers, il semble qu'il soit écrit dans le tas, ce qui provoque alors une erreur OutOfMemory et fait tomber le serveur entier.

Ces fichiers ne peuvent être servis à authentifiés les utilisateurs utilisent HTTPS, c'est pourquoi je les sers à travers un Servlet au lieu de simplement les coller dans Apache.

Le code que j'utilise est (duvet supprimé autour de ça):

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

FileInputStream ne semble pas causer de problème comme si j'écrivais dans un autre fichier ou que je supprimais complètement l'écriture.l'utilisation de la mémoire ne semble pas être un problème.

ce que je pense est que le resp.getOutputStream().write est stocké en mémoire jusqu'à ce que les données puissent être envoyées client. Ainsi, le fichier entier peut être lu et stocké dans le resp.getOutputStream() j'ai des problèmes de mémoire et je m'écrase!

j'ai essayé de tamponner ces flux et j'ai aussi essayé d'utiliser des canaux de java.nio, et aucun ne semble faire peu de différence à mes problèmes de mémoire. J'ai également vidé le OutputStream une fois par itération de la boucle, et après la boucle, ce qui n'aidait pas.

35
demandé sur Robert 2009-03-26 13:52:01

10 réponses

le servletcontainer décent moyen lui-même chasse le flux par défaut tous les ~2KB. Vous devriez vraiment pas avoir le besoin d'appeler explicitement flush() sur le OutputStreamHttpServletResponse à intervalles lors de la diffusion séquentielle des données à partir d'une seule et même source. Par exemple Tomcat (et Websphere!) ceci est configurable comme bufferSize attribut du connecteur HTTP.

le servletcontainer décent moyen diffuse aussi juste les données dans morceaux si le contenu la longueur est inconnue au préalable (selon le spécification de L'API Servlet!) et si le client supporte le protocole HTTP 1.1.

les symptômes du problème indiquent au moins que le servletcontainer est la mise en mémoire tampon du flux entier avant le rinçage. Cela peut signifier que l'en-tête content length n'est pas défini et/ou que le servletcontainer ne supporte pas l'encodage tronqué et/ou que le côté client ne supporte pas l'encodage tronqué (c'est-à-dire QU'il utilise HTTP 1.0).

Pour corriger une ou d'autres, il suffit de régler la longueur du contenu à l'avance:

response.setHeader("Content-Length", String.valueOf(new File(path).length()));
41
répondu BalusC 2010-06-16 12:38:19

flush travail sur le flux de sortie.

Vraiment je voulais faire remarquer que vous devez utiliser les trois-arg forme d'écriture que le tampon n'est pas nécessairement entièrement lu (notamment à la fin du fichier(!)). Aussi un try / finally serait en ordre sauf si vous voulez que votre serveur meurt de façon inattendue.

1
répondu Tom Hawtin - tackline 2009-03-26 11:44:53

j'ai utilisé une classe qui encapsule le outputstream pour le rendre réutilisable dans d'autres contextes. Cela a bien fonctionné pour moi d'envoyer les données au navigateur plus rapidement, mais je n'ai pas examiné les implications sur la mémoire. (veuillez pardonner mon archaïque m_ l'appellation variable)

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
répondu Kevin Hakanson 2009-03-26 15:32:02

je ne suis pas sûr si flush()ServletOutputStream fonctionne dans ce cas, mais ServletResponse.flushBuffer() doit envoyer la réponse au client (au moins par 2.3 spécifications de servlet).

ServletResponse.setBufferSize() semble prometteur, aussi.

1
répondu david a. 2009-03-30 18:42:34

donc, selon votre scénario, ne devriez-vous pas être en boucle(à chaque itération) plutôt qu'en dehors de celle-ci? J'essaierais ça, mais avec un peu plus de tampon.

1
répondu Kostas 2010-06-16 10:20:41
  1. la classe de Kevin devrait fermer le m_out champ si ce n'est pas nul dans l'opérateur close (), nous ne voulons pas divulguer des choses, n'est-ce pas?

  2. ainsi Que ServletOutputStream.flush() opérateur HttpServletResponse.flushBuffer() l'opération peut également rincer les tampons. Cependant, il semble qu'il s'agisse d'un détail spécifique à la mise en œuvre quant à savoir si ces opérations ont ou non un effet, ou si le support de la longueur du contenu http interfère. Rappelez-vous, spécifier la longueur du contenu est une option sur HTTP 1.0, donc les choses devraient juste s'échapper si vous tirez la chasse. Mais je ne vois pas ce que

1
répondu SteveL 2010-06-21 16:17:06

la condition while ne fonctionne pas, vous devez vérifier la -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 en appelant le getOutputStream() de façon répétitive.

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
répondu eckes 2011-06-24 10:17:38

sans rapport avec vos problèmes de mémoire, la boucle while devrait être:

while(bytesRead > 0);
0
répondu james 2009-03-26 14:08:51

votre code a une boucle infinie.

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

décalage a la même valeur dans la boucle, donc si au départ décalage = 0, il en sera de même à chaque itération qui causera une boucle infinie et qui conduira à une erreur.

0
répondu rooparam 2014-04-25 07:20:38

Ibm websphere application server utilise le transfert asynchrone de données pour les servlets par défaut. Cela signifie que les tampons de réponse. Si vous avez des problèmes avec les grandes données et les exceptions OutOfMemory, essayez de changer les paramètres sur était d'utiliser le mode synchrone.

Paramétrage du WebSphere Application Server WebContainer en mode synchrone

vous devez également prendre soin de charger les morceaux et les rincer. Échantillon pour chargement à partir d'un grand fichier.

ServletOutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(file);
            try {
                int buffSize = 1024;
                byte[] buffer = new byte[buffSize];
                int len;
                while ((len = fis.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                    os.flush();
                    response.flushBuffer();
                }
            } finally {
                os.close();
            }
-1
répondu zoki 2015-07-08 08:03:06