diff --git a/spring-boot-modules/spring-boot-mvc-2/README.md b/spring-boot-modules/spring-boot-mvc-2/README.md
index dc6a136131..d7341cca35 100644
--- a/spring-boot-modules/spring-boot-mvc-2/README.md
+++ b/spring-boot-modules/spring-boot-mvc-2/README.md
@@ -6,4 +6,8 @@ This module contains articles about Spring Web MVC in Spring Boot projects.
- [Functional Controllers in Spring MVC](https://www.baeldung.com/spring-mvc-functional-controllers)
- [Specify an Array of Strings as Body Parameters in Swagger](https://www.baeldung.com/swagger-body-array-of-strings)
+- [ETags for REST with Spring](https://www.baeldung.com/etags-for-rest-with-spring)
+- [Testing REST with multiple MIME types](https://www.baeldung.com/testing-rest-api-with-multiple-media-types)
+- [Testing Web APIs with Postman Collections](https://www.baeldung.com/postman-testing-collections)
+- [Spring Boot Consuming and Producing JSON](https://www.baeldung.com/spring-boot-json)
- More articles: [[prev -->]](/spring-boot-modules/spring-boot-mvc)
diff --git a/spring-boot-modules/spring-boot-mvc-2/pom.xml b/spring-boot-modules/spring-boot-mvc-2/pom.xml
index edebd41986..f527fd78f6 100644
--- a/spring-boot-modules/spring-boot-mvc-2/pom.xml
+++ b/spring-boot-modules/spring-boot-mvc-2/pom.xml
@@ -49,6 +49,27 @@
org.apache.commons
commons-lang3
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+ com.h2database
+ h2
+
+
+
+ com.thoughtworks.xstream
+ xstream
+ ${xstream.version}
@@ -103,6 +124,7 @@
com.baeldung.swagger2boot.SpringBootSwaggerApplication
2.2.0.BUILD-SNAPSHOT
+ 1.4.11.1
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/Foo.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/Foo.java
new file mode 100644
index 0000000000..e553ca1b72
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/Foo.java
@@ -0,0 +1,95 @@
+package com.baeldung.etag;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Version;
+
+@Entity
+public class Foo implements Serializable {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private long id;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Version
+ private long version;
+
+ public Foo() {
+ super();
+ }
+
+ public Foo(final String name) {
+ super();
+
+ this.name = name;
+ }
+
+ // API
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(final long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public long getVersion() {
+ return version;
+ }
+
+ public void setVersion(long version) {
+ this.version = version;
+ }
+
+ //
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ final Foo other = (Foo) obj;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ } else if (!name.equals(other.name))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("Foo [name=").append(name).append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/FooController.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/FooController.java
new file mode 100644
index 0000000000..58f366501d
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/FooController.java
@@ -0,0 +1,58 @@
+package com.baeldung.etag;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ResponseStatusException;
+
+@RestController
+@RequestMapping(value = "/foos")
+public class FooController {
+
+ @Autowired
+ private FooDao fooDao;
+
+ @GetMapping(value = "/{id}")
+ public Foo findById(@PathVariable("id") final Long id, final HttpServletResponse response) {
+ return fooDao.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ }
+
+ // Note: the global filter overrides the ETag value we set here. We can still
+ // analyze its behaviour in the Integration Test.
+ @GetMapping(value = "/{id}/custom-etag")
+ public ResponseEntity findByIdWithCustomEtag(@PathVariable("id") final Long id,
+ final HttpServletResponse response) {
+ final Foo foo = fooDao.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ return ResponseEntity.ok().eTag(Long.toString(foo.getVersion())).body(foo);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public Foo create(@RequestBody final Foo resource, final HttpServletResponse response) {
+ return fooDao.save(resource);
+ }
+
+ @PutMapping(value = "/{id}")
+ @ResponseStatus(HttpStatus.OK)
+ public void update(@PathVariable("id") final Long id, @RequestBody final Foo resource) {
+ fooDao.save(resource);
+ }
+
+ @DeleteMapping(value = "/{id}")
+ @ResponseStatus(HttpStatus.OK)
+ public void delete(@PathVariable("id") final Long id) {
+ fooDao.deleteById(id);
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/FooDao.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/FooDao.java
new file mode 100644
index 0000000000..aff011af4a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/FooDao.java
@@ -0,0 +1,8 @@
+package com.baeldung.etag;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface FooDao extends CrudRepository{
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/SpringBootEtagApplication.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/SpringBootEtagApplication.java
new file mode 100644
index 0000000000..9e58a1550c
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/SpringBootEtagApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.etag;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringBootEtagApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootEtagApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/WebConfig.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/WebConfig.java
new file mode 100644
index 0000000000..bef468452a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/etag/WebConfig.java
@@ -0,0 +1,28 @@
+package com.baeldung.etag;
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.ShallowEtagHeaderFilter;
+
+@Configuration
+public class WebConfig {
+
+ // Etags
+
+ // If we're not using Spring Boot we can make use of
+ // AbstractAnnotationConfigDispatcherServletInitializer#getServletFilters
+ @Bean
+ public FilterRegistrationBean shallowEtagHeaderFilter() {
+ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( new ShallowEtagHeaderFilter());
+ filterRegistrationBean.addUrlPatterns("/foos/*");
+ filterRegistrationBean.setName("etagFilter");
+ return filterRegistrationBean;
+ }
+
+ // We can also just declare the filter directly
+ // @Bean
+ // public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
+ // return new ShallowEtagHeaderFilter();
+ // }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/SpringBootStudentsApplication.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/SpringBootStudentsApplication.java
new file mode 100644
index 0000000000..9c499e6103
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/SpringBootStudentsApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.students;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringBootStudentsApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootStudentsApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/Student.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/Student.java
new file mode 100644
index 0000000000..16d02fe14a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/Student.java
@@ -0,0 +1,53 @@
+package com.baeldung.students;
+
+public class Student {
+
+ private long id;
+ private String firstName;
+ private String lastName;
+
+ public Student() {}
+
+ public Student(String firstName, String lastName) {
+ super();
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public Student(long id, String firstName, String lastName) {
+ super();
+ this.id = id;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @Override
+ public String toString() {
+ return "Student [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + "]";
+ }
+
+}
diff --git a/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/StudentController.java b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/StudentController.java
new file mode 100644
index 0000000000..c71bb6c6e6
--- /dev/null
+++ b/spring-boot-modules/spring-boot-mvc-2/src/main/java/com/baeldung/students/StudentController.java
@@ -0,0 +1,74 @@
+package com.baeldung.students;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+import com.baeldung.students.StudentService;
+
+@RestController
+@RequestMapping("/students")
+public class StudentController {
+
+ @Autowired
+ private StudentService service;
+
+ @GetMapping("/")
+ public List read() {
+ return service.readAll();
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity read(@PathVariable("id") Long id) {
+ Student foundStudent = service.read(id);
+ if (foundStudent == null) {
+ return ResponseEntity.notFound().build();
+ } else {
+ return ResponseEntity.ok(foundStudent);
+ }
+ }
+
+ @PostMapping("/")
+ public ResponseEntity create(@RequestBody Student student) throws URISyntaxException {
+ Student createdStudent = service.create(student);
+
+ URI uri = ServletUriComponentsBuilder.fromCurrentRequest()
+ .path("/{id}")
+ .buildAndExpand(createdStudent.getId())
+ .toUri();
+
+ return ResponseEntity.created(uri)
+ .body(createdStudent);
+
+ }
+
+ @PutMapping("/{id}")
+ public ResponseEntity update(@RequestBody Student student, @PathVariable Long id) {
+ Student updatedStudent = service.update(id, student);
+ if (updatedStudent == null) {
+ return ResponseEntity.notFound().build();
+ } else {
+ return ResponseEntity.ok(updatedStudent);
+ }
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity