Статьи

HTTP-кеш с примерами Spring

Кэширование является мощной функцией протокола HTTP, но по некоторым причинам оно в основном рассматривается для статических ресурсов, таких как изображения, таблицы стилей CSS или файлы JavaScript. Однако HTTP-кэширование не ограничивается ресурсами приложения, так как вы также можете использовать его для динамически вычисляемых ресурсов.

С небольшим объемом работы вы можете ускорить ваше приложение и улучшить общее впечатление пользователя. В этой статье вы узнаете, как использовать встроенный механизм кэширования ответов HTTP для результатов контроллера Spring .

1. Как и когда использовать HTTP-кеш ответов?

Вы можете выполнять кэширование на нескольких уровнях вашего приложения. Базы данных имеют свои кэш-хранилища, приложение кэширует некоторые данные в оперативной памяти , веб-клиент также повторно использует информацию на своей стороне.

Как вы, возможно, знаете, протокол HTTP отвечает за сетевое взаимодействие. Механизм кэширования позволяет нам оптимизировать сетевой трафик, уменьшая объем данных, передаваемых между клиентом и сервером.

Что вы можете (и должны) оптимизировать?

Когда веб-ресурс меняется не очень часто или вы точно знаете, когда он обновляется , у вас есть идеальный кандидат для оптимизации с использованием HTTP-кэша.

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

Выбор подходящих заголовков HTTP зависит от конкретного случая, который вы хотите оптимизировать. Но независимо от варианта использования мы можем разделить параметры управления кэшем в зависимости от того, где происходит проверка кэша. Это может быть проверено клиентом или сервером.

Давайте устроим это шоу на дороге.

HTTP-кеширование

2. Проверка кэша на стороне клиента

Когда вы знаете, что запрашиваемый ресурс не будет изменяться в течение заданного периода времени, сервер может отправить такую ​​информацию клиенту в качестве заголовка ответа. На основании этой информации клиент решает, следует ли ему снова извлечь ресурс или использовать ранее загруженный.

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

2.1. HTTP-кеш действителен в течение фиксированного промежутка времени

Если вы хотите запретить клиенту повторно загружать ресурс в течение заданного промежутка времени , вы должны взглянуть на заголовок Cache-Control, где вы можете указать, как долго извлеченные данные должны использоваться повторно.

Устанавливая значение заголовка max-age = <секунд>, вы информируете клиента о том, как долго в секундах ресурс не нужно снова извлекать. Срок действия кэшированного значения зависит от времени запроса.

Чтобы установить HTTP-заголовок в контроллере Spring, вместо обычного объекта полезной нагрузки вы должны вернуть класс-оболочку ResponseEntity . Вот пример:

1
2
3
4
5
6
7
8
@GetMapping("/{id}")
ResponseEntity<Product> getProduct(@PathVariable long id) {
   // …
   CacheControl cacheControl = CacheControl.maxAge(30, TimeUnit.MINUTES);
   return ResponseEntity.ok()
           .cacheControl(cacheControl)
           .body(product);
}

Значением заголовка является обычная строка, но в случае Cache-Control Spring предоставляет нам специальный класс компоновщика, который не позволяет нам делать небольшие ошибки, такие как опечатки.

2.2. HTTP-кеш действителен до фиксированной даты

Иногда вы знаете, когда ресурс изменится. Это общий случай для данных, публикуемых с некоторой частотой, таких как прогноз погоды или индикаторы фондового рынка, рассчитанные для вчерашней торговой сессии. Точная дата истечения срока действия ресурса может быть выставлена ​​клиенту.

Для этого вы должны использовать HTTP-заголовок Expires . Значение даты должно быть отформатировано с использованием одного из стандартизированных форматов данных .

1
2
3
Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format

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

1
2
3
4
5
6
7
8
9
@GetMapping("/forecast")
ResponseEntity<Forecast> getTodaysForecast() {
   // ...
   ZonedDateTime expiresDate = ZonedDateTime.now().with(LocalTime.MAX);
   String expires = expiresDate.format(DateTimeFormatter.RFC_1123_DATE_TIME);
   return ResponseEntity.ok()
           .header(HttpHeaders.EXPIRES, expires)
           .body(weatherForecast);
}

Обратите внимание, что для формата даты HTTP требуется информация о часовом поясе . Вот почему в приведенном выше примере используется ZonedDateTime . Если вы попытаетесь использовать LocalDateTime, вы получите следующее сообщение об ошибке во время выполнения:

1
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds

Если в ответе присутствуют заголовки Cache-Control и Expires , клиент использует только Cache-Control .

3. Проверка кэша на стороне сервера

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

3.1. Был ли ресурс изменен с даты?

Если вы отслеживаете дату модификации веб-ресурса, вы можете выставить такую ​​дату клиенту как часть ответа. В следующем запросе клиент отправит эту дату обратно на сервер, чтобы он мог проверить, был ли ресурс изменен с момента предыдущего запроса. Если ресурс не изменился, серверу не нужно повторно отправлять данные. Вместо этого он отвечает 304 HTTP-кодом без какой-либо полезной нагрузки.

HTTP-кеширование

Чтобы выставить дату модификации ресурса, вы должны установить заголовок Last-Modified . В построителе Spring ResponseEntity есть специальный метод lastModified (), который помогает назначать значение в правильном формате. Вы увидите это через минуту.

Но прежде чем отправлять полный ответ, вы должны проверить, включил ли клиент в запрос заголовок If-Modified-Since . Клиент устанавливает его значение на основе значения заголовка Last-Modified, который был отправлен с предыдущим ответом для этого конкретного ресурса.

Если значение заголовка If-Modified-Since совпадает с датой изменения запрошенного ресурса, вы можете сэкономить некоторую полосу пропускания и ответить клиенту пустым телом.

Опять же, Spring поставляется с вспомогательным методом, который упрощает сравнение вышеупомянутых дат. Этот метод с именем checkNotModified () можно найти в классе- оболочке WebRequest, который вы можете добавить в метод контроллера в качестве входных данных.

Звучит сложно?

Давайте внимательнее посмотрим на полный пример.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@GetMapping("/{id}")
ResponseEntity<Product> getProduct(@PathVariable long id, WebRequest request) {
   Product product = repository.find(id);
   long modificationDate = product.getModificationDate()
           .toInstant().toEpochMilli();
 
   if (request.checkNotModified(modificationDate)) {
       return null;
   }
 
   return ResponseEntity.ok()
           .lastModified(modificationDate)
           .body(product);
}

Сначала мы выбираем запрошенный ресурс и получаем доступ к дате его модификации. Мы конвертируем дату в число миллисекунд с 1 января 1970 года по Гринвичу, потому что это формат, который ожидает среда Spring.

Затем мы сравниваем дату со значением заголовка If-Modified-Since и возвращаем пустое тело в положительном совпадении. В противном случае сервер отправляет полное тело ответа с соответствующим значением заголовка Last-Modified .

Со всеми этими знаниями вы можете охватить практически всех распространенных кандидатов на кэширование. Но есть еще один важный механизм, о котором вы должны знать:

3.2. Управление версиями с помощью ETag

До сих пор мы определяли точность срока годности с точностью до одной секунды.

Но что, если вам нужна лучшая точность, чем секунда ?

Вот где приходит ETag.

ETag может быть определен как уникальное строковое значение, которое однозначно идентифицирует ресурс в данный момент времени. Обычно сервер вычисляет ETag на основе свойств данного ресурса или, если доступно, даты его последней модификации.

Поток связи между клиентом и сервером практически такой же, как и в случае проверки даты модификации. Только имена и значения заголовков отличаются.

Сервер устанавливает значение ETag в заголовке, называемом (на удивление) ETag . Когда клиент снова обращается к ресурсу, он должен отправить свое значение в заголовок с именем If-None-Match . Если это значение совпадает с вновь рассчитанным ETag для ресурса, сервер может ответить пустым телом и HTTP-кодом 304.

Весной вы можете реализовать поток сервера ETag, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@GetMapping("/{id}")
ResponseEntity<Product> getProduct(@PathVariable long id, WebRequest request) {
   Product product = repository.find(id);
   String modificationDate = product.getModificationDate().toString();
   String eTag = DigestUtils.md5DigestAsHex(modificationDate.getBytes());
 
   if (request.checkNotModified(eTag)) {
       return null;
   }
 
   return ResponseEntity.ok()
           .eTag(eTag)
           .body(product);
}

Это выглядит похоже?

Да, образец почти такой же, как предыдущий с проверкой даты модификации. Мы просто используем другое значение для сравнения (и алгоритм MD5 для вычисления ETag). Обратите внимание, что в WebRequest есть перегруженный метод checkNotModified () для работы с ETag, представленными в виде строк.

Если Last-Modified и ETag работают почти одинаково, зачем нам оба?

HTTP-кеширование

3.3. Последнее изменение против ETag

Как я уже упоминал, заголовок Last-Modified менее точен, поскольку имеет точность в одну секунду. Для большей точности выберите ETag .

Если вы не отслеживаете дату модификации ресурса, вы также вынуждены использовать ETag . Сервер может рассчитать его значение на основе свойств ресурса. Думайте об этом как хеш-код объекта.

Если у ресурса есть дата модификации и точность в одну секунду вам подходит, используйте заголовок Last-Modified . Почему? Потому что расчет ETag может быть дорогой операцией .

Кстати, стоит упомянуть, что протокол HTTP не определяет алгоритм, который вы должны использовать для вычисления ETag. При выборе алгоритма следует ориентироваться на его скорость.

Эта статья посвящена кешированию запросов GET, но вы должны знать, что сервер может использовать ETag для синхронизации операций обновления. Но это идея для другой статьи.

3.4. Пружинный фильтр ETag

Поскольку ETag — это просто строковое представление контента, сервер может вычислить его значение, используя байтовое представление ответа. Это означает, что вы можете назначить ETag любому ответу.

И угадай что?

Платформа Spring предоставляет вам реализацию фильтра ответов ETag, которая сделает это за вас. Все, что вам нужно сделать, это настроить фильтр в вашем приложении.

Самый простой способ добавить HTTP-фильтр в приложение Spring — использовать FilterRegistrationBean в вашем классе конфигурации.

1
2
3
4
5
6
7
8
@Bean
public FilterRegistrationBean filterRegistrationBean () {
   ShallowEtagHeaderFilter eTagFilter = new ShallowEtagHeaderFilter();
   FilterRegistrationBean registration = new FilterRegistrationBean();
   registration.setFilter(eTagFilter);
   registration.addUrlPatterns("/*");
   return registration;
}

В этом случае вызов addUrlPatterns () является избыточным, поскольку по умолчанию все пути совпадают. Я поместил его здесь, чтобы продемонстрировать, что вы можете контролировать, к каким ресурсам Spring должен добавить значение ETag.

Помимо генерации ETag, фильтр также отвечает HTTP 304 и пустым телом, когда это возможно.

Но будьте осторожны.

Расчет ETag может быть дорогим. Для некоторых приложений включение этого фильтра может принести больше вреда, чем пользы . Продумайте свое решение, прежде чем его использовать.

Вывод

Статья кажется довольно длинной, но мы рассмотрели много полезного материала. Теперь вы знаете, как оптимизировать ваше приложение, используя HTTP-кеш, и какой подход лучше для вас, поскольку приложения имеют разные потребности.

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

Мы также обсудили проверку на стороне сервера и сравнили заголовки Last-Modified и ETag . Наконец, вы увидели, как установить глобальный фильтр ETag в приложении Spring.

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

Смотрите оригинальную статью здесь: HTTP-кеш с примерами Spring

Мнения, высказанные участниками Java Code Geeks, являются их собственными.