Read response body in async request

John Snow :

I am wondering how to read response in filter from request body if @Controller method returns Callable interface.

My filter looks like this. Response is always empty. Any solution to this? Is this allowed only using AsyncListener?

@Component
public class ResposeBodyXmlValidator extends OncePerRequestFilter {
    private final XmlUtils xmlUtils;
    private final Resource xsdResource;

    public ResposeBodyXmlValidator(
        XmlUtils xmlUtils,
        @Value("classpath:xsd/some.xsd") Resource xsdResource
    ) {
        this.xmlUtils = xmlUtils;
        this.xsdResource = xsdResource;
    }

    @Override
    protected void doFilterInternal(
        HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain
    ) throws ServletException, IOException {
        ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(httpServletResponse);

        doFilter(httpServletRequest, response, filterChain);

        if (MediaType.APPLICATION_XML.getType().equals(response.getContentType())) {
            try {
                xmlUtils.validate(new String(response.getContentAsByteArray(), response.getCharacterEncoding()), xsdResource.getInputStream());
            } catch (IOException | SAXException e) {
                String exceptionString = String.format("Chyba při volání %s\nNevalidní výstupní XML: %s",
                    httpServletRequest.getRemoteAddr(),
                    e.getMessage());
                response.setContentType(MediaType.TEXT_PLAIN_VALUE + "; charset=UTF-8");
                response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                response.getWriter().print(exceptionString);
            }
        }
        response.copyBodyToResponse(); // I found this needs to be added at the end of the filter
    }
}
Adam Ostrožlík :

The problem of Callable is that the dispatcher servlet itself starts async processing and the filter is exited before actually processing of a request.

When Callable arrives to dispatcher servlet, it frees container thread from pool by releasing all filters (filters basically finish their work). When Callable produces results, the dispatcher servlet is called again with the same request and the response is immidiately fulfilled by the data return from Callable. This is handled by request attribute of type AsyncTaskManager which holds some information about processing of async request. This can be tested with Filter and HandlerInterceptor. Filter is executed only once but HandlerInterceptor is executed twice (original request and the request after Callable completes its job)

When you need to read request and response, one of the solution is to rewrite dispatcherServlet like this:

@Bean
@Primary
public DispatcherServlet dispatcherServlet(WebApplicationContext context) {
    return new DispatcherServlet(context) {
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
            ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
            super.service(requestWrapper, responseWrapper);
            responseWrapper.copyBodyToResponse();
        }
    };
}

This way you ensure that you can read request and response multiple times. Other thing is to add HandlerInterceptor like this (you have to pass some data as request attribute):

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
        Exception {
        Object asyncRequestData = request.getAttribute(LOGGER_FILTER_ATTRIBUTE);
        if (asyncRequestData == null) {
            request.setAttribute(LOGGER_FILTER_ATTRIBUTE, new AsyncRequestData(request));
        }
        return true;
    }

    @Override
    public void afterCompletion(
        HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex
    ) throws Exception {
        Object asyncRequestData = request.getAttribute(LOGGER_FILTER_ATTRIBUTE);
        if (asyncRequestData != null && response instanceof ContentCachingResponseWrapper) {
            log(request, (ContentCachingResponseWrapper) response, (AsyncRequestData) asyncRequestData);
        }
    }

afterCompletion method is called only once after async request has been completely processed. preHandle is called exactly twice so you have to check existance of your attribute. In afterCompletion, the response from the call is already present and if you do want to replace it, you should call response.resetBuffer().

This is one possible solution and there could be better ways.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=356587&siteId=1