ETags untuk REST dengan Spring

REST Top

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya

1. Ikhtisar

Artikel ini akan fokus pada bekerja dengan ETags di Spring , pengujian integrasi REST API dan skenario konsumsi dengan curl .

2. REST dan ETags

Dari dokumentasi resmi Musim Semi tentang dukungan ETag:

ETag (tag entitas) adalah header respons HTTP yang dikembalikan oleh server web yang mendukung HTTP / 1.1 yang digunakan untuk menentukan perubahan konten di URL tertentu.

Kita dapat menggunakan ETag untuk dua hal - caching dan permintaan bersyarat. Nilai ETag dapat dianggap sebagai hash yang dihitung dari byte dari badan Respon. Karena layanan kemungkinan besar menggunakan fungsi hash kriptografik, bahkan modifikasi terkecil dari badan akan secara drastis mengubah keluaran dan juga nilai ETag. Ini hanya berlaku untuk ETag yang kuat - protokolnya juga menyediakan Etag yang lemah.

Menggunakan header If- * mengubah permintaan GET standar menjadi GET bersyarat. Dua header If- * yang digunakan dengan ETag adalah "If-None-Match" dan "If-Match" - masing-masing dengan semantiknya sendiri seperti yang dibahas nanti di artikel ini.

3. Komunikasi Client-Server Dengan curl

Kami dapat memecah komunikasi Client-Server sederhana yang melibatkan ETag ke dalam langkah-langkah:

Pertama, Klien melakukan panggilan REST API - Responsnya mencakup header ETag yang akan disimpan untuk penggunaan lebih lanjut:

curl -H "Accept: application/json" -i //localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK ETag: "f88dd058fe004909615a64f01be66a7" Content-Type: application/json;charset=UTF-8 Content-Length: 52

Untuk permintaan berikutnya, Klien akan menyertakan header permintaan If-None-Match dengan nilai ETag dari langkah sebelumnya. Jika Sumber Daya tidak berubah di Server, Respons tidak akan berisi isi dan kode status 304 - Tidak Dimodifikasi :

curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i //localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 304 Not Modified ETag: "f88dd058fe004909615a64f01be66a7"

Sekarang, sebelum mengambil Sumber Daya lagi, mari kita ubah dengan melakukan pembaruan:

curl -H "Content-Type: application/json" -i -X PUT --data '{ "id":1, "name":"Transformers2"}' //localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK ETag: "d41d8cd98f00b204e9800998ecf8427e" Content-Length: 0

Akhirnya, kami mengirimkan permintaan terakhir untuk mengambil Foo lagi. Perlu diingat bahwa kami telah memperbaruinya sejak terakhir kali kami memintanya, sehingga nilai ETag sebelumnya seharusnya tidak berfungsi lagi. Respons akan berisi data baru dan ETag baru yang, sekali lagi, dapat disimpan untuk digunakan lebih lanjut:

curl -H "Accept: application/json" -H 'If-None-Match: "f88dd058fe004909615a64f01be66a7"' -i //localhost:8080/spring-boot-rest/foos/1
HTTP/1.1 200 OK ETag: "03cb37ca667706c68c0aad4cb04c3a211" Content-Type: application/json;charset=UTF-8 Content-Length: 56

Dan begitulah - ETags di alam liar dan menghemat bandwidth.

4. Dukungan ETag di Musim Semi

Untuk dukungan Spring: menggunakan ETag di Spring sangat mudah diatur dan sepenuhnya transparan untuk aplikasi. Kami dapat mengaktifkan dukungan dengan menambahkan Filter sederhana di web.xml :

 etagFilter org.springframework.web.filter.ShallowEtagHeaderFilter   etagFilter /foos/* 

Kami memetakan filter pada pola URI yang sama dengan RESTful API itu sendiri. Filter itu sendiri adalah implementasi standar fungsionalitas ETag sejak Spring 3.0.

Implementasinya dangkal - aplikasi menghitung ETag berdasarkan respons, yang akan menghemat bandwidth tetapi bukan kinerja server.

Jadi, permintaan yang akan mendapatkan keuntungan dari dukungan ETag akan tetap diproses sebagai permintaan standar, menggunakan sumber daya apa pun yang biasanya dikonsumsi (koneksi database, dll) dan hanya sebelum tanggapannya dikembalikan ke klien, dukungan ETag akan menendang. di.

Pada saat itu ETag akan dihitung dari badan Tanggapan dan ditetapkan pada Sumber Daya itu sendiri; juga, jika tajuk If-None-Match ditetapkan pada Permintaan, itu akan ditangani juga.

Implementasi yang lebih dalam dari mekanisme ETag berpotensi memberikan manfaat yang jauh lebih besar - seperti melayani beberapa permintaan dari cache dan tidak harus melakukan penghitungan sama sekali - tetapi penerapannya pasti tidak akan sesederhana, atau semudah pendekatan yang dangkal. dijelaskan di sini.

4.1. Konfigurasi Berbasis Java

Mari kita lihat bagaimana konfigurasi berbasis Java akan terlihat dengan mendeklarasikan kacang ShallowEtagHeaderFilter dalam konteks Spring kita :

@Bean public ShallowEtagHeaderFilter shallowEtagHeaderFilter() { return new ShallowEtagHeaderFilter(); }

Perlu diingat bahwa jika kita perlu menyediakan konfigurasi filter lebih lanjut, sebagai gantinya kita dapat mendeklarasikan instance FilterRegistrationBean :

@Bean public FilterRegistrationBean shallowEtagHeaderFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean( new ShallowEtagHeaderFilter()); filterRegistrationBean.addUrlPatterns("/foos/*"); filterRegistrationBean.setName("etagFilter"); return filterRegistrationBean; }

Akhirnya, jika kita tidak menggunakan Spring Boot kita dapat mengatur filter menggunakan AbstractAnnotationConfigDispatcherServletInitializer 's getServletFilters metode.

4.2. Menggunakan Metode eTag () ResponseEntity

Metode ini diperkenalkan di Spring framework 4.1, dan kita dapat menggunakannya untuk mengontrol nilai ETag yang diambil oleh satu titik akhir .

Misalnya, bayangkan kita menggunakan entitas berversi sebagai mekanisme Penguncian Optimis untuk mengakses informasi database kita.

Kita dapat menggunakan versinya sendiri sebagai ETag untuk menunjukkan apakah entitas telah dimodifikasi:

@GetMapping(value = "/{id}/custom-etag") public ResponseEntity findByIdWithCustomEtag(@PathVariable("id") final Long id) { // ...Foo foo = ... return ResponseEntity.ok() .eTag(Long.toString(foo.getVersion())) .body(foo); }

Layanan akan mengambil status 304-Not Modified yang sesuai jika header bersyarat permintaan cocok dengan data caching.

5. Menguji ETags

Let's start simple – we need to verify that the response of a simple request retrieving a single Resource will actually return the “ETag” header:

@Test public void givenResourceExists_whenRetrievingResource_thenEtagIsAlsoReturned() { // Given String uriOfResource = createAsUri(); // When Response findOneResponse = RestAssured.given(). header("Accept", "application/json").get(uriOfResource); // Then assertNotNull(findOneResponse.getHeader("ETag")); }

Next, we verify the happy path of the ETag behavior. If the Request to retrieve the Resource from the server uses the correct ETag value, then the server doesn't retrieve the Resource:

@Test public void givenResourceWasRetrieved_whenRetrievingAgainWithEtag_thenNotModifiedReturned() { // Given String uriOfResource = createAsUri(); Response findOneResponse = RestAssured.given(). header("Accept", "application/json").get(uriOfResource); String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG); // When Response secondFindOneResponse= RestAssured.given(). header("Accept", "application/json").headers("If-None-Match", etagValue) .get(uriOfResource); // Then assertTrue(secondFindOneResponse.getStatusCode() == 304); }

Step by step:

  • we create and retrieve a Resource, storing the ETag value
  • send a new retrieve request, this time with the “If-None-Match” header specifying the ETag value previously stored
  • on this second request, the server simply returns a 304 Not Modified, since the Resource itself has indeed not beeing modified between the two retrieval operations

Finally, we verify the case where the Resource is changed between the first and the second retrieval requests:

@Test public void givenResourceWasRetrievedThenModified_whenRetrievingAgainWithEtag_thenResourceIsReturned() { // Given String uriOfResource = createAsUri(); Response findOneResponse = RestAssured.given(). header("Accept", "application/json").get(uriOfResource); String etagValue = findOneResponse.getHeader(HttpHeaders.ETAG); existingResource.setName(randomAlphabetic(6)); update(existingResource); // When Response secondFindOneResponse= RestAssured.given(). header("Accept", "application/json").headers("If-None-Match", etagValue) .get(uriOfResource); // Then assertTrue(secondFindOneResponse.getStatusCode() == 200); }

Step by step:

  • we first create and retrieve a Resource – and store the ETag value for further use
  • then we update the same Resource
  • send a new GET request, this time with the “If-None-Match” header specifying the ETag that we previously stored
  • on this second request, the server will return a 200 OK along with the full Resource, since the ETag value is no longer correct, as we updated the Resource in the meantime

Finally, the last test – which is not going to work because the functionality has not yet been implemented in Spring – is the support for the If-Match HTTP header:

@Test public void givenResourceExists_whenRetrievedWithIfMatchIncorrectEtag_then412IsReceived() { // Given T existingResource = getApi().create(createNewEntity()); // When String uriOfResource = baseUri + "/" + existingResource.getId(); Response findOneResponse = RestAssured.given().header("Accept", "application/json"). headers("If-Match", randomAlphabetic(8)).get(uriOfResource); // Then assertTrue(findOneResponse.getStatusCode() == 412); }

Step by step:

  • we create a Resource
  • then retrieve it using the “If-Match” header specifying an incorrect ETag value – this is a conditional GET request
  • the server should return a 412 Precondition Failed

6. ETags Are Big

Kami hanya menggunakan ETag untuk operasi baca. Ada RFC yang mencoba menjelaskan bagaimana implementasi harus menangani ETag pada operasi tulis - ini bukan standar, tetapi merupakan bacaan yang menarik.

Tentu saja ada kemungkinan penggunaan lain dari mekanisme ETag, seperti untuk Mekanisme Penguncian Optimis serta menangani "Masalah Pembaruan Hilang" terkait.

Ada juga beberapa potensi jebakan dan peringatan yang harus diperhatikan saat menggunakan ETag.

7. Kesimpulan

Artikel ini hanya menggores permukaan dengan apa yang mungkin dilakukan dengan Spring dan ETags.

Untuk implementasi penuh layanan RESTful yang mendukung ETag, bersama dengan tes integrasi yang memverifikasi perilaku ETag, lihat proyek GitHub.

REST bawah

Saya baru saja mengumumkan kursus Learn Spring baru , yang berfokus pada dasar-dasar Spring 5 dan Spring Boot 2:

>> LIHAT KURSUSnya