«1. Обзор

В этом руководстве мы узнаем, как реализовать эффективную регистрацию запросов/ответов RestTemplate. Это особенно полезно для отладки обмена между двумя серверами.

К сожалению, Spring Boot не предоставляет простого способа проверить или зарегистрировать простое тело ответа JSON.

Мы собираемся изучить несколько методов логирования заголовков HTTP или, что наиболее интересно, тела HTTP.

Примечание: шаблон Spring RestTemplate устарел и будет заменен WebClient. Вы можете найти аналогичную статью с использованием WebClient здесь: Logging Spring WebClient Calls.

2. Базовое ведение журнала с помощью RestTemplate

Давайте начнем настройку регистратора RestTemplate в файле application.properties:

logging.level.org.springframework.web.client.RestTemplate=DEBUG

В результате мы можем видеть только основную информацию, такую ​​как URL-адрес запроса, метод, тело, и статус ответа:

o.s.w.c.RestTemplate - HTTP POST http://localhost:8082/spring-rest/persons
o.s.w.c.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*]
o.s.w.c.RestTemplate - Writing [my request body] with org.springframework.http.converter.StringHttpMessageConverter
o.s.w.c.RestTemplate - Response 200 OK

Тем не менее, тело ответа здесь не регистрируется, что очень жаль, потому что это самая интересная часть.

Чтобы решить эту проблему, мы выберем Apache HttpClient или перехватчик Spring.

3. Регистрация заголовков/тела с помощью Apache HttpClient

Во-первых, мы должны заставить RestTemplate использовать реализацию Apache HttpClient.

Нам понадобится зависимость Maven:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>

При создании экземпляра RestTemplate мы должны сообщить ему, что используем Apache HttpClient:

RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

Затем давайте настроим клиентский регистратор в приложении. Файл .properties:

logging.level.org.apache.http=DEBUG
logging.level.httpclient.wire=DEBUG

Теперь мы можем видеть как заголовки запроса/ответа, так и тело:

    o.a.http.headers - http-outgoing-0 >> POST /spring-rest/persons HTTP/1.1
    o.a.http.headers - http-outgoing-0 >> Accept: text/plain, application/json, application/*+json, */*
// ... more request headers
    o.a.http.headers - http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.9 (Java/1.8.0_171)
    o.a.http.headers - http-outgoing-0 >> Accept-Encoding: gzip,deflate
org.apache.http.wire - http-outgoing-0 >> "POST /spring-rest/persons HTTP/1.1[\r][\n]"
org.apache.http.wire - http-outgoing-0 >> "Accept: text/plain, application/json, application/*+json, */*[\r][\n]"
org.apache.http.wire - http-outgoing-0 >> "Content-Type: text/plain;charset=ISO-8859-1[\r][\n]"
// ... more request headers
org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
org.apache.http.wire - http-outgoing-0 >> "my request body"
org.apache.http.wire - http-outgoing-0 << "HTTP/1.1 200 [\r][\n]"
org.apache.http.wire - http-outgoing-0 << "Content-Type: application/json[\r][\n]"
// ... more response headers
org.apache.http.wire - http-outgoing-0 << "Connection: keep-alive[\r][\n]"
org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
org.apache.http.wire - http-outgoing-0 << "21[\r][\n]"
org.apache.http.wire - http-outgoing-0 << "["Lucie","Jackie","Danesh","Tao"][\r][\n]"

Однако эти журналы подробны и неудобны для отладки.

Мы увидим, как это решить в следующей главе.

4. Запись тела с помощью перехватчика RestTemplate

В качестве другого решения мы можем настроить перехватчики для RestTemplate.

4.1. Реализация Logging Interceptor

Во-первых, давайте создадим новый LoggingInterceptor для настройки наших журналов. Этот перехватчик регистрирует тело запроса как простой массив байтов. Однако для ответа мы должны прочитать весь основной поток:

public class LoggingInterceptor implements ClientHttpRequestInterceptor {

    static Logger LOGGER = LoggerFactory.getLogger(LoggingInterceptor.class);

    @Override
    public ClientHttpResponse intercept(
      HttpRequest req, byte[] reqBody, ClientHttpRequestExecution ex) throws IOException {
        LOGGER.debug("Request body: {}", new String(reqBody, StandardCharsets.UTF_8));
        ClientHttpResponse response = ex.execute(req, reqBody);
        InputStreamReader isr = new InputStreamReader(
          response.getBody(), StandardCharsets.UTF_8);
        String body = new BufferedReader(isr).lines()
            .collect(Collectors.joining("\n"));
        LOGGER.debug("Response body: {}", body);
        return response;
    }
}

Остерегайтесь, этот перехватчик влияет на само содержание ответа, как мы узнаем в следующей главе.

4.2. Использование перехватчика с RestTemplate

Теперь мы должны решить проблему потоковой передачи: поскольку перехватчик потребляет поток ответа, наше клиентское приложение увидит пустое тело ответа.

Чтобы избежать этого, мы должны использовать BufferingClientHttpRequestFactory: он буферизует содержимое потока в память. Таким образом, он может быть прочитан дважды: один раз нашим перехватчиком и второй раз нашим клиентским приложением:

ClientHttpRequestFactory factory = 
        new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
        RestTemplate restTemplate = new RestTemplate(factory);

Однако использование этой фабрики связано с недостатком производительности, который мы опишем в следующем подразделе. .

Затем мы можем добавить наш перехватчик логов в экземпляр RestTemplate — мы добавим его после существующих перехватчиков, если они есть:

List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
    interceptors = new ArrayList<>();
}
interceptors.add(new LoggingInterceptor());
restTemplate.setInterceptors(interceptors);

В результате в логах присутствует только необходимая информация:

c.b.r.l.LoggingInterceptor - Request body: my request body
c.b.r.l.LoggingInterceptor - Response body: ["Lucie","Jackie","Danesh","Tao"]

4.3. Недостаток перехватчика RestTemplate

Как упоминалось ранее, использование BufferingClientHttpRequestFactory имеет серьезный недостаток: оно сводит на нет преимущества потоковой передачи. Как следствие, загрузка всех данных тела в память может привести к проблемам с производительностью нашего приложения. Хуже того, это может привести к ошибке OutOfMemoryError.

Чтобы предотвратить это, один из возможных вариантов — предположить, что эти подробные журналы будут отключены при увеличении объема данных, что обычно происходит в рабочей среде. Например, мы можем использовать буферизованный экземпляр RestTemplate, только если в нашем регистраторе включен уровень DEBUG:

RestTemplate restTemplate = null;
if (LOGGER.isDebugEnabled()) {
    ClientHttpRequestFactory factory 
    = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
    restTemplate = new RestTemplate(factory);
} else {
    restTemplate = new RestTemplate();
}

Точно так же мы обеспечим, чтобы наш перехватчик считывал ответ только при включенном ведении журнала DEBUG:

if (LOGGER.isDebugEnabled()) {
    InputStreamReader isr = new InputStreamReader(response.getBody(), StandardCharsets.UTF_8);
    String body = new BufferedReader(isr)
        .lines()
        .collect(Collectors.joining("\n"));
    LOGGER.debug("Response body: {}", body);
}

~~ ~ 5. Заключение

Ведение журнала запросов/ответов RestTemplate не является простым делом, поскольку Spring Boot не включает его в стандартную комплектацию.

К счастью, мы увидели, что можем использовать регистратор Apache HttpClient для получения подробной трассировки обмениваемых данных.

Или мы можем реализовать собственный перехватчик, чтобы получать более удобочитаемые журналы. Однако это может привести к снижению производительности при работе с большими объемами данных.

«Как всегда, исходный код этой статьи доступен на GitHub в тестовой папке. В примере используется RestTemplate в реальном тесте для конечной точки REST, определенной в том же проекте.