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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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:/";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user