1. Введение
В этом кратком руководстве мы продемонстрируем основы регистрации входящих запросов с использованием фильтра регистрации Spring. Если мы только начинаем заниматься логированием, то можем ознакомиться с этой вводной статьей по логированию , а также со статьей SLF4J .
2. Зависимости Maven
Зависимости ведения журнала будут такими же, как и во вступительной статье; мы просто добавим здесь Spring:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
Последнюю версию для spring-core можно найти здесь .
3. Базовый веб-контроллер
Во-первых, мы определим контроллер для использования в нашем примере:
@RestController
public class TaxiFareController {
@GetMapping("/taxifare/get/")
public RateCard getTaxiFare() {
return new RateCard();
}
@PostMapping("/taxifare/calculate/")
public String calculateTaxiFare(
@RequestBody @Valid TaxiRide taxiRide) {
// return the calculated fare
}
}
4. Пользовательское ведение журнала запросов
Spring предоставляет механизм для настройки определяемых пользователем перехватчиков для выполнения действий до и после веб-запросов.
Среди перехватчиков запросов Spring одним из заслуживающих внимания интерфейсов является HandlerInterceptor
, который мы можем использовать для регистрации входящего запроса, реализуя следующие методы:
preHandle() —
мы выполняем этот метод перед фактическим методом обслуживания контроллера.afterCompletion() —
мы выполняем этот метод после того, как контроллер будет готов отправить ответ
Кроме того, Spring предоставляет реализацию интерфейса HandlerInterceptor по умолчанию в виде класса
HandlerInterceptorAdaptor
, который пользователь может расширить.
Давайте создадим собственный перехватчик, расширив HandlerInterceptorAdaptor
следующим образом:
@Component
public class TaxiFareRequestInterceptor
extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
return true;
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
//
}
}
Наконец, мы настроим TaxiRideRequestInterceptor
внутри жизненного цикла MVC для захвата предварительной и последующей обработки вызовов методов контроллера, которые сопоставляются с путем /taxifare,
определенным в классе TaxiFareController
:
@Configuration
public class TaxiFareMVCConfig implements WebMvcConfigurer {
@Autowired
private TaxiFareRequestInterceptor taxiFareRequestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(taxiFareRequestInterceptor)
.addPathPatterns("/taxifare/*/");
}
}
В заключение, WebMvcConfigurer
добавляет TaxiFareRequestInterceptor
в жизненный цикл Spring MVC, вызывая метод addInterceptors()
.
Самая большая проблема состоит в том, чтобы получить копии полезной нагрузки запроса и ответа для ведения журнала и при этом оставить запрошенную полезную нагрузку для обработки сервлетом:
Основная проблема с запросом на чтение заключается в том, что, как только входной поток читается в первый раз, он помечается как потребляемый и не может быть прочитан снова.
Приложение выдаст исключение после чтения потока запроса:
{
"timestamp": 1500645243383,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.http.converter
.HttpMessageNotReadableException",
"message": "Could not read document: Stream closed;
nested exception is java.io.IOException: Stream closed",
"path": "/rest-log/taxifare/calculate/"
}
Чтобы решить эту проблему , мы можем использовать кэширование для хранения потока запросов и использования его для ведения журнала.
Spring предоставляет несколько полезных классов, таких как ContentCachingRequestWrapper и ContentCachingResponseWrapper , которые можно использовать для кэширования данных запроса в целях ведения журнала.
Давайте настроим наш preHandle()
класса TaxiRideRequestInterceptor
для кэширования объекта запроса с помощью класса ContentCachingRequestWrapper
:
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
HttpServletRequest requestCacheWrapperObject
= new ContentCachingRequestWrapper(request);
requestCacheWrapperObject.getParameterMap();
// Read inputStream from requestCacheWrapperObject and log it
return true;
}
Как мы видим, мы кэшируем объект запроса с помощью класса ContentCachingRequestWrapper
, который мы можем использовать для чтения полезных данных для ведения журнала, не нарушая фактический объект запроса:
requestCacheWrapperObject.getContentAsByteArray();
Ограничение
- Класс
ContentCachingRequestWrapper
поддерживает только следующее:
Content-Type:application/x-www-form-urlencoded
Method-Type:POST
- Мы должны вызвать следующий метод, чтобы гарантировать, что данные запроса кэшируются в
ContentCachingRequestWrapper
перед его использованием:
requestCacheWrapperObject.getParameterMap();
5. Встроенная регистрация запросов Spring
Spring предоставляет встроенное решение для регистрации полезной нагрузки. Мы можем использовать готовые фильтры, подключившись к приложению Spring с помощью конфигурации.
AbstractRequestLoggingFilter
— это фильтр, обеспечивающий основные функции ведения журнала. Подклассы должны переопределять методы beforeRequest()
и afterRequest()
для фактического логирования запроса.
Платформа Spring предоставляет три конкретных класса реализации, которые мы можем использовать для регистрации входящего запроса. Вот эти три класса:
CommonsRequestLoggingFilter
Log4jNestedDiagnosticContextFilter
(устаревший)ServletContextRequestLoggingFilter
Теперь давайте перейдем к CommonsRequestLoggingFilter
и настроим его для захвата входящих запросов для ведения журнала.
5.1. Настройка приложения Spring Boot
Мы можем настроить приложение Spring Boot, добавив определение bean-компонента, чтобы включить ведение журнала запросов:
@Configuration
public class RequestLoggingFilterConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter
= new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setMaxPayloadLength(10000);
filter.setIncludeHeaders(false);
filter.setAfterMessagePrefix("REQUEST DATA : ");
return filter;
}
}
Этот фильтр ведения журнала также требует, чтобы мы установили уровень журнала DEBUG. Мы можем включить режим DEBUG, добавив следующий элемент в logback.xml
:
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter">
<level value="DEBUG" />
</logger>
Другой способ включить журнал уровня DEBUG — добавить в application.properties
следующее :
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=
DEBUG
5.2. Настройка традиционного веб-приложения
В стандартном веб-приложении Spring мы можем установить фильтр
с помощью конфигурации XML или конфигурации Java. Итак, давайте настроим CommonsRequestLoggingFilter,
используя обычную конфигурацию на основе Java.
Как мы знаем, для атрибута includePayload
фильтра CommonsRequestLoggingFilter
по умолчанию установлено значение false. Нам понадобится пользовательский класс, чтобы переопределить значение атрибута, чтобы включить includePayload
перед внедрением в контейнер с использованием конфигурации Java:
public class CustomeRequestLoggingFilter
extends CommonsRequestLoggingFilter {
public CustomeRequestLoggingFilter() {
super.setIncludeQueryString(true);
super.setIncludePayload(true);
super.setMaxPayloadLength(10000);
}
}
Затем нам нужно внедрить CustomeRequestLoggingFilter,
используя веб-инициализатор на основе Java :
public class CustomWebAppInitializer implements
WebApplicationInitializer {
public void onStartup(ServletContext container) {
AnnotationConfigWebApplicationContext context
= new AnnotationConfigWebApplicationContext();
context.setConfigLocation("com.foreach");
container.addListener(new ContextLoaderListener(context));
ServletRegistration.Dynamic dispatcher
= container.addServlet("dispatcher",
new DispatcherServlet(context));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
container.addFilter("customRequestLoggingFilter",
CustomeRequestLoggingFilter.class)
.addMappingForServletNames(null, false, "dispatcher");
}
}
6. Пример в действии
Наконец, мы можем подключить Spring Boot с контекстом, чтобы увидеть в действии, что ведение журнала входящих запросов работает должным образом:
@Test
public void givenRequest_whenFetchTaxiFareRateCard_thanOK() {
TestRestTemplate testRestTemplate = new TestRestTemplate();
TaxiRide taxiRide = new TaxiRide(true, 10l);
String fare = testRestTemplate.postForObject(
URL + "calculate/",
taxiRide, String.class);
assertThat(fare, equalTo("200"));
}
7. Заключение
В этой статье мы узнали, как реализовать базовое ведение журнала веб-запросов с помощью перехватчиков. Мы также изучили ограничения и проблемы этого решения.
Затем мы обсудили встроенный класс фильтра, который предоставляет готовые к использованию и простые механизмы регистрации.
Как всегда, реализация примера и фрагменты кода доступны на GitHub.