异步Spring MVC控制器方法后的日志响应体

时间:2017-08-29 17:30:39

标签: java spring spring-mvc asynchronous completable-future

我的Spring MVC应用程序中有以下内容:

@RestController
public class SomeController {
   @GetMapping(value = "/csv", produces = { "text/csv", MediaType.APPLICATION_JSON_VALUE })
   public Future someAsyncMethod() {
        return CompletableFuture
            .supplyAsync(() -> generateCsvSlowly()))
            .thenApply(csv -> {
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.add("Content-Disposition", "attachment; filename=" + "Filename_.csv");
                httpHeaders.add("Cookie", "fileDownload=true; path=/");

                return new HttpEntity<>(csv, httpHeaders);
            });
        }
    }
}

所以它只是简单地生成csv,但速度很慢,我必须使这个调用异步。

我正在尝试以下列方式记录所有响应正文:

@Component
public class LoggingFilter extends OncePerRequestFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
    private static final AtomicLong ID = new AtomicLong();

    static final String SOME_FORMAT_STRING = ...;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        long id = ID.incrementAndGet();

        HttpServletResponse responseToUse = response;
        if (!(response instanceof ContentCachingResponseWrapper)) {
            responseToUse = new ContentCachingResponseWrapper(response);
        }

        try {
            filterChain.doFilter(request, responseToUse);
        }
        finally {
            byte[] responseBodyBytes = ((ContentCachingResponseWrapper) responseToUse).getContentAsByteArray();
            LOGGER.info(SOME_FORMAT_STRING, id, responseToUse.getStatus(), responseToUse.getContentType(),
                new ServletServerHttpResponse(responseToUse).getHeaders(), new String(bodyBytes, UTF_8));
            ((ContentCachingResponseWrapper) responseToUse).copyBodyToResponse();
        }
    }

}

这是我的异常处理程序:

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ApplicationException.class)
    @ResponseBody
    public ResponseEntity<Status> handleException(ApplicationException exception) {
        Status status = new Status();
        ...

        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        ...

        return new ResponseEntity(status, headers, exception.getHttpCodeMvc());
    }

    @Override
    protected ResponseEntity handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return handleException(new ApplicationException(ex, ApplicationStatus.GENERIC_ERROR));
    }
}

此处Status是简单的POJO,ApplicationException是自定义异常类。

generateSlowlyCsv抛出异常时,它会在handleException中处理,但没有记录任何内容,也没有任何正文返回给客户端。其他非异步控制器方法记录错误(即使是同一个)就好了并返回响应体。 当生成csv(我在调试器中看到它)没有错误时,调用只是挂起而我无法找到它(它从可完成的未来返回)。如果没有LoggingFilter,一切正常,但当然没有日志。

如何在发生异常时松开响应体并在生成异常时返回csv?非常感谢你!

P.S。从控制器方法返回CallableMvcAsyncTask也无济于事

2 个答案:

答案 0 :(得分:0)

我结束了以下事情:

package com.ololo.filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicLong;

import javax.servlet.http.HttpServletRequest;

import static java.lang.System.lineSeparator;
import static java.nio.charset.StandardCharsets.UTF_8;

@ControllerAdvice
public class LoggingAdvice implements ResponseBodyAdvice {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoggingAdvice.class);
    private static final AtomicLong ID = new AtomicLong();

    private static final String SOME_RESPONSE_MESSAGE_FORMAT;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        long id = ID.incrementAndGet();

        ServletServerHttpResponse responseToUse = (ServletServerHttpResponse) response;
        HttpMessageConverter httpMessageConverter;
        LoggingHttpOutboundMessageWrapper httpOutputMessage = new LoggingHttpOutboundMessageWrapper();
        try {
            httpMessageConverter = (HttpMessageConverter) selectedConverterType.newInstance();
            httpMessageConverter.write(body, selectedContentType, httpOutputMessage);
            LOGGER.info(SOME_RESPONSE_MESSAGE_FORMAT, id, responseToUse.getServletResponse().getStatus(), responseToUse.getServletResponse().getContentType(),
                    responseToUse.getHeaders(), httpOutputMessage.getResponseBodyInString());
        } catch (InstantiationException | IllegalAccessException | IOException e) {
            e.printStackTrace();
        }

        return body;
    }

    private static final class LoggingHttpOutboundMessageWrapper implements HttpOutputMessage {
        private HttpHeaders httpHeaders = new HttpHeaders();
        private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        @Override
        public OutputStream getBody() throws IOException {
            return byteArrayOutputStream;
        }

        @Override
        public HttpHeaders getHeaders() {
            return httpHeaders;
        }

        public String getResponseBodyInString() {
            return new String(byteArrayOutputStream.toByteArray());
        }
    }

}

是的,我知道,它真可怕,但至少它适用于所有@ResponseBody控制器方法。我不认为(至少现在)我需要别的东西。我尝试编写异步过滤器,但我无法将AsyncListener添加到AsyncContext(有关详细信息,请参阅the following question)。希望这会对你有所帮助。

答案 1 :(得分:0)

我有一个类似的问题。

来自the spring.io blog post

所有Spring Framework Servlet过滤器实现已根据需要进行了修改,以在异步请求处理中工作。至于任何其他过滤器,有些将起作用-通常是那些进行预处理的过滤器,而另一些则需要修改-通常是那些在请求结束时进行后处理的过滤器。此类过滤器将需要识别何时退出了初始Servlet容器线程,从而让另一个线程继续处理,以及何时将它们作为异步调度的一部分调用以完成处理。

org.springframework.web.filter.ShallowEtagHeaderFilter是一个很好的例子。

通过在需要的地方ShallowEtagHeaderFilter处从doFilter复制方法来完成这项工作:

    if (!isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) {
        response = new ContentCachingResponseWrapper((HttpServletResponse) response);
    }

然后在末尾:

    if (!isAsyncStarted(request)) {
        updateResponse(response);
    }