«1. Обзор
В этом уроке мы узнаем о кэшировании HTTP. Мы также рассмотрим различные способы реализации этого механизма между клиентом и приложением Spring MVC.
2. Введение HTTP-кэширования
Когда мы открываем веб-страницу в браузере, он обычно загружает много ресурсов с веб-сервера:
Например, в этом примере браузеру необходимо загрузить три ресурса для одна страница /логин. Браузер обычно делает несколько HTTP-запросов для каждой веб-страницы. Теперь, если мы запрашиваем такие страницы очень часто, это вызывает большой сетевой трафик и требует больше времени для обслуживания этих страниц.
Чтобы снизить нагрузку на сеть, протокол HTTP позволяет браузерам кэшировать некоторые из этих ресурсов. Если этот параметр включен, браузеры могут сохранять копию ресурса в локальном кеше. В результате браузеры могут обслуживать эти страницы из локального хранилища, а не запрашивать их по сети:
Веб-сервер может указать браузеру кэшировать конкретный ресурс, добавив в ответ заголовок 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/baeldung"))
.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/baeldung"))
.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. Управление кешем в перехватчиках
Мы можем использовать перехватчики в нашем приложении 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/baeldung"))
.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 предоставляет нам два заголовка ответа:
- Etag – an HTTP response header that stores a unique hash value to determine whether a cached resource has changed on the server – a corresponding If-None-Match request header must carry the last Etag value
- LastModified – an HTTP response header that stores a unit of time when the resource was last updated – a corresponding If-Unmodified-Since request header must carry the last modified date
«Мы можем использовать любой из этих заголовков, чтобы проверить, нужно ли повторно извлечь ресурс с истекшим сроком действия. После проверки заголовков сервер может либо повторно отправить ресурс, либо отправить 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/baeldung").headers(headers))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().is(304));
}
~ ~~ 7. Заключение
В этой статье мы узнали о кэшировании HTTP с помощью заголовка ответа Cache-Control в Spring MVC. Мы можем либо добавить заголовок в ответ контроллера, используя класс ResponseEntity, либо через сопоставление ресурсов для статических ресурсов.
Мы также можем добавить этот заголовок для определенных шаблонов URL, используя перехватчики Spring.
Как всегда, код доступен на GitHub.