Introduce API evolution

Show, through an old/new client/server how to implement backwards compatibility using Spring HATEOAS, thereby eliminating the need to version between domain object changes.
This commit is contained in:
Greg Turnquist
2017-05-30 12:04:13 -05:00
parent a423412ed5
commit fa3940690b
36 changed files with 1261 additions and 10 deletions

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.examples;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author Greg Turnquist
*/
@Configuration
public class ClientConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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<Long> {
private Long id;
private String name;
private String role;
Employee(String name, String role) {
this.name = name;
this.role = role;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.examples;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.client.Traverson;
import org.springframework.hateoas.mvc.TypeReferences.ResourcesType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.client.RestTemplate;
/**
* A web controller that serves up client data found on a remote REST service.
*
* @author Greg Turnquist
*/
@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;
}
/**
* 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<Resource<Employee>> employees = client
.follow("employees")
.toObject(new ResourcesType<Resource<Employee>>(){});
model.addAttribute("employee", new Employee());
model.addAttribute("employees", employees);
return "index";
}
/**
* 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:/";
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.examples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Greg Turnquist
*/
@SpringBootApplication
public class OriginalClientApplication {
public static void main(String... args) {
SpringApplication.run(OriginalClientApplication.class, args);
}
}

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring HATEOAS Examples - Original Client</title>
<style>
table {
border-collapse: collapse;
}
td, th {
border: 1px solid #999;
padding: 0.5rem;
text-align: left;
}
</style>
</head>
<body>
<h1>Spring HATEOAS Examples - Original Client</h1>
<p>
This is the <b>original client</b>, and it was coded to talk to the <b>original server</b>,
but with a little design and thought, we can have it to talk to the <b>new server</b> as well!
</p>
<table>
<thead>
<tr>
<th>Name</th><th>Role</th><th>Links</th>
</tr>
</thead>
<tbody>
<tr th:each="employee : ${employees}">
<td th:text="${employee.content.name}" />
<td th:text="${employee.content.role}" />
<td>
<ul>
<li th:each="link : ${employee.links}">
<a th:text="${link.rel}" th:href="${link.href}" />
</li>
</ul>
</td>
</tr>
</tbody>
</table>
<form method="post" th:action="@{/employees}" th:object="${employee}">
<input type="text" th:field="*{name}" placeholder="Name" />
<input type="text" th:field="*{role}" placeholder="Role"/>
<input type="submit" value="Submit" />
</form>
</body>
</html>