#spring-mvc #servlets #spring-boot #servlet-filters
#spring-mvc #сервлеты #spring-boot #сервлет-фильтры
Вопрос:
Я пытаюсь реализовать фильтр для регистрации запросов и ответов в Spring MVC
приложении. Я использую следующий код:
@Component
public class LoggingFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
LOGGER.debug(REQUEST_MESSAGE_FORMAT, requestWrapper.getRequestURI(), requestWrapper.getMethod(), requestWrapper.getContentType(),
new ServletServerHttpRequest(requestWrapper).getHeaders(), IOUtils.toString(requestWrapper.getInputStream(), UTF_8));
filterChain.doFilter(requestWrapper, responseWrapper);
LOGGER.debug(RESPONSE_MESSAGE_FORMAT, responseWrapper.getStatus(), responseWrapper.getContentType(),
new ServletServerHttpResponse(responseWrapper).getHeaders(), IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8));
}
}
Итак, я получаю свой запрос и отвечаю, как и ожидалось. Вот журналы:
2016-10-08 19:10:11.212 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
URI: /resources/1
Http-Method: GET
Content-Type: null
Headers: {User-Agent=[curl/7.41.0], Accept=[*/*], Host=[localhost:9015]}
Body:
--------------------------------------
2016-10-08 19:10:11.277 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
Response-Code: 200
Content-Type: application/json;charset=UTF-8
Headers: {}
Body: {"id":"1"}
--------------------------------------
Однако возвращается пустой ответ. Вот вывод из curl
:
$ curl http://localhost:9015/resources/1 --verbose
* Trying ::1...
* Connected to localhost (::1) port 9015 (#0)
> GET /resources/1 HTTP/1.1
> User-Agent: curl/7.41.0
> Host: localhost:9015
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 08 Oct 2016 17:10:11 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Есть идеи?
Спасибо
Ответ №1:
После нескольких часов борьбы я, наконец, нашел решение.
Короче говоря, ContentCachingResponseWrapper.copyBodyToResponse()
должен вызываться в конце метода filter .
ContentCachingResponseWrapper
кэширует тело ответа, считывая его из потока вывода ответа. Таким образом, поток становится пустым. Для записи ответа обратно в выходной поток ContentCachingResponseWrapper.copyBodyToResponse()
следует использовать.
Комментарии:
1. они должны добавить это в javadoc ContentCachingResponseWrapper
2. Почему фильтр выполняет обработку ответа ?.
3.
end
означает ли последняя строка метода? что, если не последняя строка? я пробовал и тоже работает. я боюсь, что последняя строка повлияет на задержку пользователя?
Ответ №2:
Наконец-то решена проблема. Вот идеальное решение:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Enumeration;
import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
import static net.logstash.logback.marker.Markers.appendFields;
@Component
public class LoggingFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
@Autowired
private ObjectMapper objectMapper;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpServletRequest);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
filterChain.doFilter(requestWrapper, responseWrapper);
String requestUrl = requestWrapper.getRequestURL().toString();
HttpHeaders requestHeaders = new HttpHeaders();
Enumeration headerNames = requestWrapper.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
requestHeaders.add(headerName, requestWrapper.getHeader(headerName));
}
HttpMethod httpMethod = HttpMethod.valueOf(requestWrapper.getMethod());
Map<String, String[]> requestParams = requestWrapper.getParameterMap();
String requestBody = IOUtils.toString(requestWrapper.getInputStream(),UTF_8);
JsonNode requestJson = objectMapper.readTree(requestBody);
RequestEntity<JsonNode> requestEntity = new RequestEntity<>(requestJson,requestHeaders, httpMethod, URI.create(requestUrl));
LOGGER.info(appendFields(requestEntity),"Logging Http Request");
HttpStatus responseStatus = HttpStatus.valueOf(responseWrapper.getStatusCode());
HttpHeaders responseHeaders = new HttpHeaders();
for (String headerName : responseWrapper.getHeaderNames()) {
responseHeaders.add(headerName, responseWrapper.getHeader(headerName));
}
String responseBody = IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8);
JsonNode responseJson = objectMapper.readTree(responseBody);
ResponseEntity<JsonNode> responseEntity = new ResponseEntity<>(responseJson,responseHeaders,responseStatus);
LOGGER.info(appendFields(responseEntity),"Logging Http Response");
responseWrapper.copyBodyToResponse();
}
}
Комментарии:
1. Как вы зарегистрировали этот фильтр
2. Аннотация @Component автоматически зарегистрирует этот фильтр.
3. Зачем вам нужен WebUtils.getNativeResponse там?
Ответ №3:
Шаблон, который мне нравится использовать, состоит в том, чтобы разделить его на 2 фильтра: один для извлечения необработанного тела, а другой для ведения журнала — чувствует себя более SRP.
@Slf4j // lombok logging
@Component // spring loads filter into it's filter chain
@Order(1) // Best if this goes first (or early in filter chain)
public class CachingBodyFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper((HttpServletRequest) req);
ContentCachingResponseWrapper resWrapper = new ContentCachingResponseWrapper((HttpServletResponse) res);
try {
chain.doFilter(reqWrapper, resWrapper);
resWrapper.copyBodyToResponse(); // Necessary (see answer by StasKolodyuk above)
} catch (IOException | ServletException e) {
log.error("Error extracting body", e);
}
}
}
И затем мы создаем другой фильтр для выполнения части протоколирования.
@Slf4j
@Component
@Order(2) // This needs to come after `CachingBodyFilter`
public class PayloadLogFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
chain.doFilter(req, res);
if (req instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper reqWrapper = (ContentCachingRequestWrapper) req;
String payload = new String (reqWrapper.getContentAsByteArray(), "utf-8");
log.debug("Request [ {} ] has payload [ {} ]", reqWrapper.getRequestURI(), payload);
}
}
}
Приятным преимуществом их разделения является то, что другие классы (например, перехватчик Spring AOP или Spring controller) также могут получать доступ / использовать тело HTTP.