web-dev-qa-db-fra.com

Comment lire et copier le contenu du flux de sortie de réponse de la servlet HTTP pour la journalisation

J'ai créé un filtre dans mon Java (appengine en fait) qui enregistre les paramètres d'une demande entrante. Je voudrais également enregistrer la réponse résultante que mon serveur Web écrit. Bien que je avoir accès à l'objet de réponse, je ne sais pas comment en obtenir la réponse chaîne/contenu réelle.

Des idées?

44
aloo

Vous devez créer une Filter dans laquelle vous encapsulez l'argument ServletResponse avec une implémentation personnalisée HttpServletResponseWrapper dans laquelle vous remplacez la getOutputStream() et getWriter() pour renvoyer une implémentation personnalisée ServletOutputStream dans laquelle vous copiez les octets écrits dans l'abstrait de base OutputStream#write(int b) méthode. Ensuite, vous passez à la place l'appel personnalisé enveloppé HttpServletResponseWrapper à l'appel FilterChain#doFilter() et enfin vous devriez pouvoir obtenir la réponse copiée après l'appel.

En d'autres termes, le Filter:

@WebFilter("/*")
public class ResponseLogger implements Filter {

    @Override
    public void init(FilterConfig config) throws ServletException {
        // NOOP.
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (response.getCharacterEncoding() == null) {
            response.setCharacterEncoding("UTF-8"); // Or whatever default. UTF-8 is good for World Domination.
        }

        HttpServletResponseCopier responseCopier = new HttpServletResponseCopier((HttpServletResponse) response);

        try {
            chain.doFilter(request, responseCopier);
            responseCopier.flushBuffer();
        } finally {
            byte[] copy = responseCopier.getCopy();
            System.out.println(new String(copy, response.getCharacterEncoding())); // Do your logging job here. This is just a basic example.
        }
    }

    @Override
    public void destroy() {
        // NOOP.
    }

}

Le HttpServletResponseWrapper personnalisé:

public class HttpServletResponseCopier extends HttpServletResponseWrapper {

    private ServletOutputStream outputStream;
    private PrintWriter writer;
    private ServletOutputStreamCopier copier;

    public HttpServletResponseCopier(HttpServletResponse response) throws IOException {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (writer != null) {
            throw new IllegalStateException("getWriter() has already been called on this response.");
        }

        if (outputStream == null) {
            outputStream = getResponse().getOutputStream();
            copier = new ServletOutputStreamCopier(outputStream);
        }

        return copier;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (outputStream != null) {
            throw new IllegalStateException("getOutputStream() has already been called on this response.");
        }

        if (writer == null) {
            copier = new ServletOutputStreamCopier(getResponse().getOutputStream());
            writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
        }

        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (writer != null) {
            writer.flush();
        } else if (outputStream != null) {
            copier.flush();
        }
    }

    public byte[] getCopy() {
        if (copier != null) {
            return copier.getCopy();
        } else {
            return new byte[0];
        }
    }

}

Le ServletOutputStream personnalisé:

public class ServletOutputStreamCopier extends ServletOutputStream {

    private OutputStream outputStream;
    private ByteArrayOutputStream copy;

    public ServletOutputStreamCopier(OutputStream outputStream) {
        this.outputStream = outputStream;
        this.copy = new ByteArrayOutputStream(1024);
    }

    @Override
    public void write(int b) throws IOException {
        outputStream.write(b);
        copy.write(b);
    }

    public byte[] getCopy() {
        return copy.toByteArray();
    }

}
97
BalusC

La solution BalusC est correcte, mais peu dépassée. Le printemps a maintenant une fonctionnalité pour cela. Il vous suffit d'utiliser [ContentCachingResponseWrapper], Qui a la méthode public byte[] getContentAsByteArray().

Je suggère de faire WrapperFactory qui permettra de le rendre configurable, que ce soit pour utiliser ResponseWrapper ou ContentCachingResponseWrapper par défaut.

9
omilus

Alors que la réponse de BalusC fonctionnera dans la plupart des scénarios, vous devez être prudent avec l'appel flush - il valide la réponse et aucune autre écriture n'est possible, par exemple. via les filtres suivants. Nous avons trouvé des problèmes avec une approche très similaire dans un environnement Websphere où la réponse fournie n'était que partielle.

Selon cette question le flush ne devrait pas être appelé du tout et vous devriez le laisser être appelé en interne.

J'ai résolu le problème de vidage en utilisant TeeWriter (il divise le flux en 2 flux) et en utilisant des flux sans mise en mémoire tampon dans le "flux ramifié" à des fins de journalisation. Il n'est alors pas nécessaire d'appeler le flush.

private HttpServletResponse wrapResponseForLogging(HttpServletResponse response, final Writer branchedWriter) {
    return new HttpServletResponseWrapper(response) {
        PrintWriter writer;

        @Override
        public synchronized PrintWriter getWriter() throws IOException {
            if (writer == null) {
                writer = new PrintWriter(new TeeWriter(super.getWriter(), branchedWriter));
            }
            return writer;
        }
    };
}

Ensuite, vous pouvez l'utiliser de cette façon:

protected void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
    //...
    StringBuilderWriter branchedWriter = new org.Apache.commons.io.output.StringBuilderWriter();
    try {
        chain.doFilter(request, wrapResponseForLogging(response, branchedWriter));
    } finally {
        log.trace("Response: " + branchedWriter);
    }
}

Le code est simplifié pour la brasserie.

4
Petr Újezdský

Je ne connais pas très bien Appengine mais vous avez besoin de quelque chose Access Log Valve dans Tomcat. Son modèle d'attribut ; une présentation de mise en forme identifiant les différents champs d'informations de la demande et de la réponse à enregistrer, ou le mot commun ou combiné pour sélectionner un format standard.

Il semble qu'appengine ait une fonctionnalité intégrée pour filtrage des journaux .

appliquer un filtre de servlet

3

Au lieu de créer un HttpServletResponseWrapper personnalisé, vous pouvez utiliser ContentCachingResponseWrapper car il fournit la méthode getContentAsByteArray ().

public void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
            FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = servletRequest;
        HttpServletResponse response = servletResponse;
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper =new ContentCachingResponseWrapper(response);
        try {
            super.doFilterInternal(requestWrapper, responseWrapper, filterChain);

        } finally {

            byte[] responseArray=responseWrapper.getContentAsByteArray();
            String responseStr=new String(responseArray,responseWrapper.getCharacterEncoding());
            System.out.println("string"+responseStr);       
            /*It is important to copy cached reponse body back to response stream
            to see response */
            responseWrapper.copyBodyToResponse();

        }

    }
2
Shrinivasan