Перейти к основному содержимому

Заголовки кеша в Spring MVC

· 6 мин. чтения

1. Обзор

В этом руководстве мы узнаем о кэшировании HTTP. Мы также рассмотрим различные способы реализации этого механизма между клиентом и приложением Spring MVC.

2. Представляем HTTP-кеширование

Когда мы открываем веб-страницу в браузере, она обычно загружает много ресурсов с веб-сервера:

./8fe5cfd190b94c0521aa96c76fd79fae.jpg

Например, в этом примере браузеру необходимо загрузить три ресурса для одной страницы /login . Браузер обычно делает несколько HTTP-запросов для каждой веб-страницы. Теперь, если мы запрашиваем такие страницы очень часто, это вызывает большой сетевой трафик и требует больше времени для обслуживания этих страниц .

Чтобы уменьшить нагрузку на сеть, протокол HTTP позволяет браузерам кэшировать некоторые из этих ресурсов. Если этот параметр включен, браузеры могут сохранять копию ресурса в локальном кеше. В результате браузеры могут обслуживать эти страницы из локального хранилища, а не запрашивать их по сети:

./1d278f7c53343a2ebc3ce33df5cae60d.jpg

Веб-сервер может указать браузеру кэшировать конкретный ресурс, добавив в ответ заголовок Cache-Control .

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

В следующих разделах мы добавим этот заголовок в ответ от контроллера Spring MVC. Позже мы также увидим API-интерфейсы Spring для проверки кэшированных ресурсов на основе времени истечения срока действия.

3. Cache-Control в ответе контроллера

3.1. Использование ResponseEntity

Самый простой способ сделать это — использовать класс компоновщика CacheControl , предоставляемый Spring :

@GetMapping("/hello/{name}")
@ResponseBody
public ResponseEntity<String> hello(@PathVariable String name) {
CacheControl cacheControl = CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate();
return ResponseEntity.ok()
.cacheControl(cacheControl)
.body("Hello " + name);
}

Это добавит в ответ заголовок Cache-Control :

@Test
void whenHome_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/hello/foreach"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}

3.2. Использование HttpServletResponse

Часто контроллерам необходимо вернуть имя представления из метода обработчика. Однако класс ResponseEntity не позволяет нам одновременно возвращать имя представления и обрабатывать тело запроса .

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

@GetMapping(value = "/home/{name}")
public String home(@PathVariable String name, final HttpServletResponse response) {
response.addHeader("Cache-Control", "max-age=60, must-revalidate, no-transform");
return "home";
}

Это также добавит заголовок Cache-Control в ответ HTTP, аналогичный последнему разделу:

@Test
void whenHome_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/home/foreach"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"))
.andExpect(MockMvcResultMatchers.view().name("home"));
}

4. Cache-Control для статических ресурсов

Как правило, наше приложение Spring MVC обслуживает множество статических ресурсов , таких как файлы HTML, CSS и JS. Поскольку такие файлы потребляют много трафика в сети, браузерам важно кэшировать их. Мы снова включим это с заголовком Cache-Control в ответе.

Spring позволяет нам контролировать это поведение кэширования при сопоставлении ресурсов:

@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/")
.setCacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate());
}

Это гарантирует, что все ресурсы, определенные в /resources , возвращаются с заголовком Cache-Control в ответе .

5. Cache-Control в перехватчиках

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

Теперь вместо реализации собственного перехватчика мы будем использовать WebContentInterceptor , предоставляемый Spring :

@Override
public void addInterceptors(InterceptorRegistry registry) {
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.addCacheMapping(CacheControl.maxAge(60, TimeUnit.SECONDS)
.noTransform()
.mustRevalidate(), "/login/*");
registry.addInterceptor(interceptor);
}

Здесь мы зарегистрировали WebContentInterceptor и добавили заголовок Cache-Control , аналогичный последним нескольким разделам. Примечательно, что мы можем добавлять разные заголовки Cache-Control для разных шаблонов URL.

В приведенном выше примере для всех запросов, начинающихся с /login , мы добавим этот заголовок:

@Test
void whenInterceptor_thenReturnCacheHeader() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/login/foreach"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.header()
.string("Cache-Control","max-age=60, must-revalidate, no-transform"));
}

6. Проверка кеша в Spring MVC

До сих пор мы обсуждали различные способы включения заголовка Cache-Control в ответ. Это указывает, что клиенты или браузеры должны кэшировать ресурсы на основе свойств конфигурации, таких как max-age .

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

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

  1. Etag — заголовок ответа HTTP, в котором хранится уникальное хеш-значение для определения того, изменился ли кэшированный ресурс на сервере — соответствующий заголовок запроса If-None-Match должен содержать последнее значение Etag.
  2. LastModified — заголовок ответа HTTP, в котором хранится единица времени последнего обновления ресурса — соответствующий заголовок запроса If-Unmodified-Since должен содержать дату последнего изменения.

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

Заголовок LastModified может хранить только временные интервалы с точностью до секунд. Это может быть ограничением в тех случаях, когда требуется более короткий срок действия. По этой причине вместо этого рекомендуется использовать Etag . Поскольку в заголовке Etag хранится хеш-значение , можно создать уникальный хэш с более мелкими интервалами, такими как наносекунды.

Тем не менее, давайте посмотрим, как выглядит использование LastModified.

Spring предоставляет несколько служебных методов для проверки того, содержит ли запрос заголовок срока действия или нет:

@GetMapping(value = "/productInfo/{name}")
public ResponseEntity<String> validate(@PathVariable String name, WebRequest request) {

ZoneId zoneId = ZoneId.of("GMT");
long lastModifiedTimestamp = LocalDateTime.of(2020, 02, 4, 19, 57, 45)
.atZone(zoneId).toInstant().toEpochMilli();

if (request.checkNotModified(lastModifiedTimestamp)) {
return ResponseEntity.status(304).build();
}

return ResponseEntity.ok().body("Hello " + name);
}

Spring предоставляет метод checkNotModified() для проверки того, был ли ресурс изменен с момента последнего запроса:

@Test
void whenValidate_thenReturnCacheHeader() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.add(IF_UNMODIFIED_SINCE, "Tue, 04 Feb 2020 19:57:25 GMT");
this.mockMvc.perform(MockMvcRequestBuilders.get("/productInfo/foreach").headers(headers))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(304));
}

7. Заключение

В этой статье мы узнали о кэшировании HTTP с помощью заголовка ответа Cache-Control в Spring MVC. Мы можем либо добавить заголовок в ответ контроллера, используя класс ResponseEntity , либо через сопоставление ресурсов для статических ресурсов.

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

Как всегда, код доступен на GitHub .