diff --git a/README.adoc b/README.adoc index 0f35eaa..9211ca6 100644 --- a/README.adoc +++ b/README.adoc @@ -11,5 +11,5 @@ We have separate folders for each of these: == Spring HATEOAS Modules -* `basics` - Poke and prod at a hypermedia-powered service from inside the code as well as externally using standard tools -* `upgrade` - Upgrade an existing REST resource +* link:basics[Basics] - Poke and prod at a hypermedia-powered service from inside the code as well as externally using standard tools +* link:api-evolution[API Evolution] - Upgrade an existing REST resource diff --git a/api-evolution/README.adoc b/api-evolution/README.adoc new file mode 100644 index 0000000..0df2518 --- /dev/null +++ b/api-evolution/README.adoc @@ -0,0 +1,440 @@ += Spring HATEOAS - API Evolution Example + +This guide shows a valuable example of using Spring HATEOAS. It illustrates how to evolve your API while maintaining backward compatible. +This is valuable because it reduces the need to https://www.infoq.com/articles/roy-fielding-on-versioning[version your API], a concept not suitable for REST services. + +This guide also assumes you have already checked out link:../basics[Basics]. + +Start with a very simple example, a payroll system that tracks employees. Create a server and a client. Then, +evolve the server while ensuring the original client can talk to the new one. Finally, upgrade the client and +take advantage of the new features provided by the server. + +NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. + +== Creating the Original Server + +=== Defining Your Original Domain + +We all must start somewhere. So imagine you created an employee representation like this: + +[source,java] +---- +@Data +@NoArgsConstructor +@Entity +class Employee implements Identifiable { + + @Id @GeneratedValue + private Long id; + private String name; + private String role; + + Employee(String name, String role) { + + this.name = name; + this.role = role; + } +} +---- + +This domain object captures an employee's name and role, along with a unique identifier for the data store. + +* `@Data` is a Lombok annotation to turn it into a mutable value type. +* `@NoArgsConstructor` creates an empty constructor, helping Jackson serialize. +* `@Entity` is a JPA annotation allowing us to store it in the H2 in-memory data store used in this example. + +IMPORTANT: Why is there no `@JsonIgnoreProperties(ignoreUnknown = true)` annotation as shown in link:../basics/src/main/java/org/springframework/hateoas/examples/Employee.java#L50[the basics example]? +Truth be told, it's not needed. Spring HATEOAS's `HypermediaSupportBeanDefinitionRegistrar` automatically disables `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`, +so you don't have to include it in your POJO definition. The *basics* example puts it in to clarify the importance of ignoring unneeded fields. + +=== Accessing Data + +With this domain object, your original server now needs a Spring Data JPA repository: + +[source,java] +---- +interface EmployeeRepository extends CrudRepository { +} +---- + +By extending Spring Data Common's `CrudRepository` and plugging in our domain object and it's id's type, we gain access to a fleet +of CRUD operations. + +=== Converting Entities to Resources + +A key component in using Spring HATEOAS to build hypermedia is transforming objects into *resources*. To do that you +need a *resource assembler* as shown in <<../basics/README.adoc#converting-entities-to-resources,basics>>. + +=== Creating a Controller + +The link:../basics/src/main/java/org/springframework/hateoas/examples/EmployeeController.java[basics example] shows how to define the two most important links: + +. one for the collection (*/employees*) +. one for an individual entity (*/employees/1*) + +That's not enough to operate. You also need to create new employees as well as navigate from the root. + +Start by adding a *root node* for clients to start from. The idea is to find relevant collections: + +[source,java] +---- +@GetMapping(value = "/", produces = MediaTypes.HAL_JSON_VALUE) +public ResourceSupport root() { + + ResourceSupport rootResource = new ResourceSupport(); + + rootResource.add( + linkTo(methodOn(EmployeeController.class).root()).withSelfRel(), + linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); + + return rootResource; +} +---- + +Since the root node only needs to serve links, creating a bare `ResourceSupport` object is quite sufficient. + +* Add a link to `EmployeeController.root` for a proper *self* link +* It also adds a link to `EmployeeController.findAll` and names it *employees*. + +From here, the client can navigate (we'll see how a little further down) to the collection of employees, as shown in *basics*. +From there, we need to add the ability to create new employees: + +[source,java] +---- +@PostMapping("/employees") +public ResponseEntity> newEmployee(@RequestBody Employee employee) { + + Employee savedEmployee = repository.save(employee); + + return ResponseEntity + .created(linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).toUri()) + .body(assembler.toResource(savedEmployee)); +} +---- + +This API can now process *PUT* requests, deserializing JSON found in the HTTP request body into an `Employee` record. + +From there, it will save it using `EmployeeRepository.save`, getting back a record that includes the id. + +Finally, Spring MVC's `ResponseEntity.created` factory method is used to: + +* Find the new employee's URI and load it into the response's *Location* header. +* Convert the newly saved `Employee` into a resource using the *assembler* and return that in the response body. + +NOTE: There's no need to show the test data loaded up in link:original-server/src/main/java/org/springframework/hateoas/examples/InitDatabase.java[InitDatabase.java] class. Just take a peek! + +=== Running a Server on a Different Port + +No need to show the rest of the server here. It's vanilla Spring Boot. But one key thing, since this example runs both +the server _and_ the client on the same machine, is to run the server on a different port. + +To do so, just add `src/main/resources/application.yml` like this: + +[source,yml] +---- +server: + port: 9000 +---- + +This will fire the thing up on port 9000. + +== Creating a RESTful client + +With our original server built, serving up employee data, it's time to switch focus to the original client. + +In this scenario, you'll build a web app with Thymeleaf templates, but retrieves some of its data from the server app you just built. + +This requires a couple extra dependencies:; + +* spring-boot-starter-thymeleaf - for Thymeleaf templating +* json-path - you'll see why shortly + +=== Creating the Client's Domain Object + +Despite what you may think, it's best that the client have its _own_ version of the `Employee`: + +[source,java] +---- +@Data +@NoArgsConstructor +class Employee { + + private Long id; + private String name; + private String role; +} +---- + +There are many advantages: + +* Decouples the client from the server. +* Clients may not want ALL the fields. +* This client doesn't talk to a data store, so no JPA annotations. +* This client isn't used to form links, to no need to implement the `Identifiable` interface. +* This client isn't used to fashion test data (yet), so no need for special constructors. + +All in all, it's enough to give it the empty constructor so Jackson can handle serializing/deserializing data over the wire. + +The real gold is in the `HomeController` used to talk to the server: + +[source,java] +---- +@Controller +public class HomeController { + + private static final String REMOTE_SERVICE_ROOT_URI = "http://localhost:9000"; + + private final RestTemplate rest; + + public HomeController(RestTemplate restTemplate) { + this.rest = restTemplate; + } + ... +} +---- + +This controller, used to construct HTML pages through Thymeleaf, needs to know the root URI of the remote +service. So in this example, it is hard coded into place. + +WARNING: For fault tolerant production systems, hard coded URIs are NOT recommended. Instead, use something like +Spring Cloud Netflix and it's Eureka/Ribbon features to allow https://spring.io/guides/gs/service-registration-and-discovery/[service discovery] and https://spring.io/guides/gs/client-side-load-balancing/[load balanced calls]. + +Parts of the controller must also perform REST calls, so we request a `RestTemplate` in the constructor call, allowing Spring to provide it. + +To construct a listing of all employees, check out the following controller method: + +[source,java] +---- +/** + * Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then + * "hopping" to the {@literal employees} rel. + * + * NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry with + * the Thymeleaf template. + * + * @param model + * @return + * @throws URISyntaxException + */ +@GetMapping +public String index(Model model) throws URISyntaxException { + + Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON); + Resources> employees = client + .follow("employees") + .toObject(new ResourcesType>(){}); + + model.addAttribute("employee", new Employee()); + model.addAttribute("employees", employees); + + return "index"; +} +---- + +Presuming you already understand Spring MVC, let's focus on the RESTful bits. + +* `Traverson` is used to start from the root node (*REMOTE_SERVICE_ROOT_URI*) and "hop" to *employees*. +Then it fetches an object, and transforms it into Spring HATEOAS's vendor neutral `Resources>` structure. +* Using this, we are able to construct a `Model` object for the template. +** An *employee* object is created to hold an empty, form-backed bean. +** *employees* is loaded up with the entire Spring HATEOAS structure, allowing the template to use what bits it wants. + +The method then returns the name of the template to render (`index`). + +NOTE: `Traverson` is what requires having *json-path* on the classpath. + +It isn't necessary to post ALL of the Thymeleaf template `index.html`, but the critical parts are here: + +[source,html] +---- + + + + + + + + + + + +
NameRoleLinks
+ + + +
+---- +This shows the employee data being served up inside an HTML table. + +* `th:each="employee : ${employees}"` lets your iterate over each one. +* `th:text="${employee.content.name}"` navigates the `Resource` structure (remmeber, you're iterating over each entry of `Resources<>`). +* `${employee.links}` gives each entry access to a Spring HATEOAS `Link`. +* `` lets you show the end user each link, both name and URI. + +Just below the HTML table is a form for creating new employees: + +[source,html] +---- +
+ + + +
+---- + +This is pure Thymeleaf. It takes the form-backed bean you just saw (`th:object="${employee}"`) +and maps the HTML inputs onto its fields. + +WARNING: You _could_ put the remote service's *employees* URI, but that would subvert standard web security tactics. +Instead, it's best that all POSTs get sent back to the client's server piece, and from there, forwarded to the remote service (just below). + +With the client put together, the last step is to forward *POST /employees* calls to the remote service: + +[source,java] +---- +/** + * Instead of putting the creation link from the remote service in the template (a security concern), + * have a local route for {@literal POST} requests. Gather up the information, and form a remote call, + * using {@link Traverson} to fetch the {@literal employees} {@link Link}. + * + * Once a new employee is created, redirect back to the root URL. + * + * @param employee + * @return + * @throws URISyntaxException + */ +@PostMapping("/employees") +public String newEmployee(@ModelAttribute Employee employee) throws URISyntaxException { + + Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON); + Link employeesLink = client + .follow("employees") + .asLink(); + + this.rest.postForEntity(employeesLink.expand().getHref(), employee, Employee.class); + + return "redirect:/"; +} +---- + +Again, you could hard code the path to */employees* on the remote service, but that would subvert REST. Instead, +you can use Traverson to open a connection to the remote service's root URI and "hop" to *employees*. But instead +of asking for the data, you just want the link. + +Using the link, `RestTemplate.postForEntity` is used to forward the data submitted in the client. Finally, a +`redirect:/` is issued to Spring MVC, telling it to navigate back to the root page. + +NOTE: It's true that *POST /employees* on the remote service will give you back an `Employee` object wrapped in HAL, +but for this example, it's not needed. Can you imagine a scenario where this information could be put to use while +redirecting the page back to home? + +== Evolving the Server + +Let's assume someone decides to update the server. This can be done in a way that doesn't cause existing clients to break. + +Looking into link:new-server[new-server], the updated `Employee` domain object can be seen: + +[source,java] +---- +@Data +@NoArgsConstructor +@Entity +class Employee implements Identifiable { + + @Id @GeneratedValue + private Long id; + private String firstName; + private String lastName; + private String role; + + Employee(String firstName, String lastName, String role) { + + this.firstName = firstName; + this.lastName = lastName; + this.role = role; + } + ... +} +---- + +The data changes to be made are shown here: + +* The single *name* field has been replaced with *firstName* and *lastName*. +* The constructor call has also been adjusted to support this. + +This is the part that would typically break things and force either a SOAP or CORBA update to be issued +for all clients. In REST, the goal is to _not_ break everyone, but instead provide a smoother experience + +The first step is to provide a "virtual" attribute. Since the original client expects a *name* field, create one! + +[source,java] +---- +/** + * Just merge {@literal firstName} and {@literal lastName} together. + * + * @return + */ +public String getName() { + return this.firstName + " " + this.lastName; +} +---- + +This simple getter method concatenates *firstName* and *lastName* together. And Jackson will automatically turn it into a *name* field. + +[source,javascript] +---- +{ + "firstName" : "Frodo", + "lastName" : "Baggins", + "name" : "Frodo Baggins", + "_links" : { + "self" : { + "href" : "http://localhost:9000/employees/1" + }, + "employees" : { + "href" : "http://localhost:9000/employees" + } + } +} +---- + +When the client receives this document over the wire, it will deserialize it into its own `Employee` domain object, throwing away +the *firstName* and *lastName* fields. + +NOTE: Concerned about sending the same information twice? Don't be. By adding just a few bytes, the cost of maintaining _two_ versions of this API +has been eliminated. If performance of a few bytes is hypercritical to the business needs at hand, then REST may not be the answer for you. + +So what happens when the original client attempts to create a new employee? You have to be able to handle that. Naturally, +you must code the setter method for this virtual *name* field: + +[source,java] +---- +/** + * Split things up, and assign the first token to {@literal firstName} with everything else to {@literal lastName}. + * + * @param wholeName + */ +public void setName(String wholeName) { + + String[] parts = wholeName.split(" "); + this.firstName = parts[0]; + if (parts.length > 1) { + this.lastName = StringUtils.arrayToDelimitedString(Arrays.copyOfRange(parts, 1, parts.length), " "); + } else { + this.lastName = ""; + } +} +---- + +This method contains the gory details of splitting up a name into parts, putting the first into *firstName*, and putting +the rest into *lastName*. + +From here on, the link:new-client[client can also evolve] and take advantage of the extra fields. + +WARNING: The example code for that doesn't depict the new-client talking to the old-server. + +This is but a simple example of making clients and services support each other through typical breaking changes. \ No newline at end of file diff --git a/api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/Employee.java b/api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/Employee.java index 62afbf6..bdbf505 100644 --- a/api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -18,8 +18,6 @@ package org.springframework.hateoas.examples; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.hateoas.Identifiable; - /** * An updated domain object on the client side. It doesn't need all the backward compatible bits that the new * server needs (unless this becomes a service of its own). @@ -28,17 +26,10 @@ import org.springframework.hateoas.Identifiable; */ @Data @NoArgsConstructor -class Employee implements Identifiable { +class Employee { private Long id; private String firstName; private String lastName; private String role; - - Employee(String firstName, String lastName, String role) { - - this.firstName = firstName; - this.lastName = lastName; - this.role = role; - } } diff --git a/api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/Employee.java b/api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/Employee.java index 0c58eda..ad39b9a 100644 --- a/api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -52,8 +52,8 @@ class Employee implements Identifiable { } /** - * Just merged {@literal firstName} and {@literal lastName} together. - * + * Just merge {@literal firstName} and {@literal lastName} together. + * * @return */ public String getName() { diff --git a/api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/Employee.java b/api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/Employee.java index 74a151d..0a9ba2f 100644 --- a/api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/Employee.java +++ b/api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -18,22 +18,14 @@ package org.springframework.hateoas.examples; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.hateoas.Identifiable; - /** * @author Greg Turnquist */ @Data @NoArgsConstructor -class Employee implements Identifiable { +class Employee { private Long id; private String name; private String role; - - Employee(String name, String role) { - - this.name = name; - this.role = role; - } } diff --git a/api-evolution/original-client/src/main/resources/templates/index.html b/api-evolution/original-client/src/main/resources/templates/index.html index ac7ae38..ddcd81e 100644 --- a/api-evolution/original-client/src/main/resources/templates/index.html +++ b/api-evolution/original-client/src/main/resources/templates/index.html @@ -43,7 +43,7 @@ - +
diff --git a/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/EmployeeController.java b/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/EmployeeController.java index 70ac0ab..5cd2e78 100644 --- a/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/EmployeeController.java +++ b/api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/EmployeeController.java @@ -47,7 +47,7 @@ class EmployeeController { public ResourceSupport root() { ResourceSupport rootResource = new ResourceSupport(); - + rootResource.add( linkTo(methodOn(EmployeeController.class).root()).withSelfRel(), linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); @@ -64,7 +64,7 @@ class EmployeeController { public ResponseEntity> newEmployee(@RequestBody Employee employee) { Employee savedEmployee = repository.save(employee); - + return ResponseEntity .created(linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).toUri()) .body(assembler.toResource(savedEmployee)); diff --git a/basics/README.adoc b/basics/README.adoc index fe1fa7f..319027b 100644 --- a/basics/README.adoc +++ b/basics/README.adoc @@ -79,6 +79,7 @@ interface EmployeeRepository extends CrudRepository { This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD) operations. +[[converting-entities-to-resources]] == Converting Entities to Resources In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as information on _how_ to