Tiêu đề bộ nhớ đệm trong Spring MVC

1. Khái quát chung

Trong hướng dẫn này, chúng ta sẽ tìm hiểu về bộ đệm HTTP. Chúng tôi cũng sẽ xem xét các cách khác nhau để triển khai cơ chế này giữa ứng dụng khách và ứng dụng Spring MVC.

2. Giới thiệu HTTP Caching

Khi chúng ta mở một trang web trên một trình duyệt, nó thường tải xuống rất nhiều tài nguyên từ máy chủ web:

Ví dụ, trong ví dụ này, một trình duyệt cần tải xuống ba tài nguyên cho một / trang đăng nhập . Việc một trình duyệt thực hiện nhiều yêu cầu HTTP cho mọi trang web là điều bình thường. Bây giờ, nếu chúng tôi yêu cầu các trang như vậy rất thường xuyên, nó sẽ gây ra rất nhiều lưu lượng truy cập mạng và mất nhiều thời gian hơn để phục vụ các trang này .

Để giảm tải mạng, giao thức HTTP cho phép trình duyệt lưu vào bộ đệm một số tài nguyên này. Nếu được bật, các trình duyệt có thể lưu bản sao của một tài nguyên trong bộ đệm ẩn cục bộ. Do đó, các trình duyệt có thể phân phát các trang này từ bộ nhớ cục bộ thay vì yêu cầu nó qua mạng:

Máy chủ web có thể hướng trình duyệt lưu vào bộ nhớ cache một tài nguyên cụ thể bằng cách thêm tiêu đề Cache-Control trong phản hồi.

Vì các tài nguyên được lưu trong bộ nhớ cache dưới dạng bản sao cục bộ, nên có nguy cơ cung cấp nội dung cũ từ trình duyệt . Do đó, máy chủ web thường thêm thời gian hết hạn trong tiêu đề Cache-Control .

Trong các phần tiếp theo, chúng tôi sẽ thêm tiêu đề này trong phản hồi từ bộ điều khiển Spring MVC. Sau đó, chúng ta cũng sẽ thấy các API mùa xuân để xác thực tài nguyên được lưu trong bộ nhớ cache dựa trên thời gian hết hạn.

3. Kiểm soát bộ nhớ cache trong phản hồi của bộ điều khiển

3.1. Sử dụng ResponseEntity

Cách đơn giản nhất để làm điều này là sử dụng lớp xây dựng CacheControl được cung cấp bởi Spring :

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

Thao tác này sẽ thêm tiêu đề Cache-Control trong phản hồi:

@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. Sử dụng HttpServletResponse

Thông thường, bộ điều khiển cần trả về tên chế độ xem từ phương thức xử lý. Tuy nhiên, lớp ResponseEntity không cho phép chúng ta trả lại tên dạng xem và xử lý thân yêu cầu cùng một lúc .

Ngoài ra, đối với các bộ điều khiển như vậy, chúng ta có thể đặt tiêu đề Cache-Control trong HttpServletResponse trực tiếp:

@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"; }

Điều này cũng sẽ thêm tiêu đề Cache-Control trong phản hồi HTTP tương tự như phần cuối cùng:

@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. Kiểm soát bộ nhớ cache cho tài nguyên tĩnh

Nói chung, ứng dụng Spring MVC của chúng tôi cung cấp rất nhiều tài nguyên tĩnh như tệp HTML, CSS và JS. Vì các tệp như vậy tiêu tốn rất nhiều băng thông mạng, vì vậy điều quan trọng là các trình duyệt phải lưu vào bộ nhớ cache của chúng. Chúng tôi sẽ bật lại điều này với tiêu đề Cache-Control trong phản hồi.

Spring cho phép chúng tôi kiểm soát hành vi bộ nhớ đệm này trong ánh xạ tài nguyên:

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

Điều này đảm bảo rằng tất cả các tài nguyên được định nghĩa dưới / tài nguyên được trả về với tiêu đề Cache-Control trong phản hồi .

5. Kiểm soát bộ nhớ cache trong bộ đánh chặn

Chúng tôi có thể sử dụng các bộ chặn trong ứng dụng Spring MVC của mình để thực hiện một số xử lý trước và sau cho mọi yêu cầu. Đây là một trình giữ chỗ khác, nơi chúng tôi có thể kiểm soát hành vi lưu vào bộ nhớ đệm của ứng dụng.

Bây giờ thay vì triển khai một trình đánh chặn tùy chỉnh, chúng tôi sẽ sử dụng Trình đánh chặn WebContentInterceptor do Spring cung cấp :

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

Ở đây, chúng tôi đã đăng ký WebContentInterceptor và thêm tiêu đề Cache-Control tương tự như một vài phần trước. Đáng chú ý, chúng ta có thể thêm các tiêu đề Cache-Control khác nhau cho các mẫu URL khác nhau.

Trong ví dụ trên, đối với tất cả các yêu cầu bắt đầu bằng / đăng nhập , chúng tôi sẽ thêm tiêu đề này:

@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. Xác thực bộ nhớ đệm trong Spring MVC

Cho đến nay, chúng ta đã thảo luận về nhiều cách khác nhau để đưa tiêu đề Cache-Control vào phản hồi. Điều này cho biết các máy khách hoặc trình duyệt lưu tài nguyên vào bộ nhớ cache dựa trên các thuộc tính cấu hình như max-age .

Nói chung, bạn nên thêm thời gian hết hạn bộ nhớ cache với mỗi tài nguyên . Do đó, các trình duyệt có thể tránh cung cấp tài nguyên đã hết hạn từ bộ nhớ cache.

Mặc dù các trình duyệt phải luôn kiểm tra xem hết hạn sử dụng, nhưng có thể không cần thiết phải tìm nạp lại tài nguyên mỗi lần. Nếu một trình duyệt có thể xác nhận rằng một tài nguyên không thay đổi trên máy chủ, thì nó có thể tiếp tục cung cấp phiên bản đã lưu trong bộ nhớ cache của nó. Và cho mục đích này, HTTP cung cấp cho chúng ta hai tiêu đề phản hồi:

  1. Etag - tiêu đề phản hồi HTTP lưu trữ giá trị băm duy nhất để xác định xem tài nguyên được lưu trong bộ nhớ cache có thay đổi trên máy chủ hay không - tiêu đề yêu cầu If-None-Match tương ứng phải mang giá trị Etag cuối cùng
  2. LastModified - tiêu đề phản hồi HTTP lưu trữ một đơn vị thời gian khi tài nguyên được cập nhật lần cuối - tiêu đề yêu cầu If-Unmodified-Since tương ứng phải có ngày sửa đổi cuối cùng

Chúng tôi có thể sử dụng một trong hai tiêu đề này để kiểm tra xem tài nguyên đã hết hạn có cần được tìm nạp lại hay không. Sau khi xác thực tiêu đề, máy chủ có thể gửi lại tài nguyên hoặc gửi mã HTTP 304 để biểu thị không có thay đổi . Đối với trường hợp sau, các trình duyệt có thể tiếp tục sử dụng tài nguyên được lưu trong bộ nhớ cache.

The LastModified header can only store time intervals up to seconds precision. This can be a limitation in cases where a shorter expiry is required. For this reason, it's recommended to use Etag instead. Since Etag header stores a hash value, it's possible to create a unique hash up to more finer intervals like nanoseconds.

That said, let's check out what it looks like to use LastModified.

Spring provides some utility methods to check if the request contains an expiration header or not:

@GetMapping(value = "/productInfo/{name}") public ResponseEntity 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 provides the checkNotModified() method to check if a resource has been modified since the last request:

@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. Conclusion

Trong bài viết này, chúng ta đã tìm hiểu về bộ đệm HTTP bằng cách sử dụng tiêu đề phản hồi Cache-Control trong Spring MVC. Chúng ta có thể thêm tiêu đề trong phản hồi của bộ điều khiển bằng cách sử dụng lớp ResponseEntity hoặc thông qua ánh xạ tài nguyên cho các tài nguyên tĩnh.

Chúng tôi cũng có thể thêm tiêu đề này cho các mẫu URL cụ thể bằng cách sử dụng trình chặn Spring.

Như mọi khi, mã có sẵn trên GitHub.