REST易于构建和使用,因此已迅速成为在Web上构建Web服务的实际标准。
关于REST如何适用于微服务世界,还有很多讨论,但是-在本教程中-让我们来看构建RESTful服务。
为什么要REST?REST包含Web的戒律,包括其体系结构,优势和其他所有内容。鉴于其作者Roy Fielding参与了十二个规范网络操作的规范,这不足为奇。
有什么好处?Web及其核心协议HTTP提供了一系列功能:
-
合适的行动(
GET
,POST
,PUT
,DELETE
,...) -
快取
-
重定向和转发
-
安全性(加密和身份验证)
这些都是构建弹性服务的关键因素。但这还不是全部。网络是建立在许多微小的规格之上的,因此它能够轻松发展,而不会陷入“标准之战”。
开发人员可以利用实现这些不同规格的第三方工具包,立即拥有客户端和服务器技术。
通过在HTTP之上进行构建,REST API提供了以下构建方法:
-
向后兼容的API
-
可进化的API
-
可扩展的服务
-
安全的服务
-
无状态到有状态服务的范围
重要的是要认识到,REST本身无处不在,而是一个标准,而是架构上的一种方法,一种样式,一组约束,可以帮助您构建Web规模的系统。在本教程中,我们将使用Spring产品组合来构建RESTful服务,同时利用REST的无堆栈功能。
入门
在学习本教程时,我们将使用Spring Boot。转到Spring Initializr并将以下依赖项添加到项目中:
-
网页
-
JPA
-
H2
将名称更改为“工资单”,然后选择“生成项目”。一个.zip
会下载。解压缩。在内部,您将找到一个简单的基于Maven的项目,其中包括一个pom.xml
构建文件(注意:您可以使用Gradle。本教程中的示例将基于Maven。)
Spring Boot可以与任何IDE一起使用。您可以使用Eclipse,IntelliJ IDEA,Netbeans等。Spring Tool Suite是基于Eclipse的开源IDE发行版,它提供Eclipse的Java EE发行版的超集。它包含的功能使使用Spring应用程序的工作变得更加轻松。绝不是必需的。但是,如果您想为按键提供额外的魅力,请考虑一下。这是一个演示如何开始使用STS和Spring Boot的视频。这是使您熟悉这些工具的一般介绍。
到目前为止的故事...
让我们从我们可以构造的最简单的东西开始。实际上,为了使其尽可能简单,我们甚至可以省略REST的概念。(稍后,我们将添加REST以了解它们之间的区别。)
大图:我们将创建一个简单的工资服务来管理公司的员工。我们将员工对象存储在(H2内存中)数据库中,并(通过称为JPA的方式)访问它们。然后,我们将使用允许通过Internet访问的内容(称为Spring MVC层)进行包装。
以下代码在我们的系统中定义了一个Employee。
package payroll;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
class Employee {
private @Id @GeneratedValue Long id;
private String name;
private String role;
Employee() {}
Employee(String name, String role) {
this.name = name;
this.role = role;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getRole() {
return this.role;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setRole(String role) {
this.role = role;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Employee))
return false;
Employee employee = (Employee) o;
return Objects.equals(this.id, employee.id) && Objects.equals(this.name, employee.name)
&& Objects.equals(this.role, employee.role);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.name, this.role);
}
@Override
public String toString() {
return "Employee{" + "id=" + this.id + ", name='" + this.name + '\'' + ", role='" + this.role + '\'' + '}';
}
}
尽管很小,但此Java类包含许多内容:
-
@Entity
是一个JPA批注,以使该对象准备好存储在基于JPA的数据存储区中。 -
id
,name
和role
是我们Employee域对象的属性。id
标有更多的JPA批注以指示它是主键,并由JPA提供程序自动填充。 -
当我们需要创建新实例但还没有ID时,会创建一个自定义构造函数。
有了这个领域对象定义,我们现在可以转向Spring Data JPA来处理繁琐的数据库交互。
Spring Data JPA存储库是具有支持针对后端数据存储创建,读取,更新和删除记录的方法的接口。在适当的情况下,某些存储库还支持数据分页和排序。Spring Data根据在接口中的方法命名中找到的约定来综合实现。
除了JPA,还有多种存储库实现。您可以使用Spring Data MongoDB,Spring Data GemFire,Spring Data Cassandra等。对于本教程,我们将坚持使用JPA。 |
Spring使访问数据变得容易。通过简单地声明以下EmployeeRepository
接口,我们将自动能够
-
创造新员工
-
更新现有的
-
删除员工
-
查找员工(一个或全部,或按简单或复杂属性搜索)
package payroll;
import org.springframework.data.jpa.repository.JpaRepository;
interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
为了获得所有这些免费功能,我们要做的就是声明一个扩展Spring Data JPA的接口,将JpaRepository
域类型指定为Employee
,将id类型指定为Long
。
Spring Data的存储库解决方案可以避开数据存储细节,而可以使用特定于域的术语解决大多数问题。
信不信由你,这足以启动一个应用程序!Spring Boot应用程序至少是一个public static void main
入口点和@SpringBootApplication
注释。这告诉Spring Boot尽可能地提供帮助。
package payroll;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PayrollApplication {
public static void main(String... args) {
SpringApplication.run(PayrollApplication.class, args);
}
}
@SpringBootApplication
是一个元注释,可引入组件扫描,自动配置和属性支持。在本教程中,我们不会深入探讨Spring Boot的细节,但从本质上讲,它将启动servlet容器并提供我们的服务。
尽管如此,没有数据的应用程序并不是很有趣,所以让我们预加载它。Follow类将在Spring之前自动加载:
package payroll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class LoadDatabase {
private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);
@Bean
CommandLineRunner initDatabase(EmployeeRepository repository) {
return args -> {
log.info("Preloading " + repository.save(new Employee("Bilbo Baggins", "burglar")));
log.info("Preloading " + repository.save(new Employee("Frodo Baggins", "thief")));
};
}
}
加载后会发生什么?
-
CommandLineRunner
一旦加载了应用程序上下文,Spring Boot将运行所有Bean。 -
该跑步者将要求
EmployeeRepository
您提供刚刚创建的副本。 -
使用它,它将创建两个实体并将其存储。
右键单击并运行 PayRollApplication
,这是您得到的:
... 2018-08-09 11:36:26.169信息74611 --- [main] payroll.LoadDatabase:预加载员工(id = 1,name = Bilbo Baggins,role = burglar) 2018-08-09 11:36:26.174信息74611 --- [main] payroll.LoadDatabase:预加载员工(id = 2,name = Frodo Baggins,role = thief) ...
这不是完整的日志,而只是预加载数据的关键部分。(的确,请查看整个控制台。这很荣耀。)
HTTP是平台
要使用Web层包装存储库,必须使用Spring MVC。多亏了Spring Boot,几乎没有基础代码可以使用。相反,我们可以专注于操作:
package payroll;
import java.util.List;
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.RestController;
@RestController
class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
// Aggregate root
// tag::get-aggregate-root[]
@GetMapping("/employees")
List<Employee> all() {
return repository.findAll();
}
// end::get-aggregate-root[]
@PostMapping("/employees")
Employee newEmployee(@RequestBody Employee newEmployee) {
return repository.save(newEmployee);
}
// Single item
@GetMapping("/employees/{id}")
Employee one(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id));
}
@PutMapping("/employees/{id}")
Employee replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) {
return repository.findById(id)
.map(employee -> {
employee.setName(newEmployee.getName());
employee.setRole(newEmployee.getRole());
return repository.save(employee);
})
.orElseGet(() -> {
newEmployee.setId(id);
return repository.save(newEmployee);
});
}
@DeleteMapping("/employees/{id}")
void deleteEmployee(@PathVariable Long id) {
repository.deleteById(id);
}
}
-
@RestController
指示每种方法返回的数据将直接写入响应主体中,而不呈现模板。 -
An
EmployeeRepository
由构造函数注入到控制器中。 -
我们的路线为每个操作(
@GetMapping
,@PostMapping
,@PutMapping
和@DeleteMapping
,对应于HTTPGET
,POST
,PUT
和DELETE
电话)。(注意:阅读每种方法并了解它们的作用非常有用。) -
EmployeeNotFoundException
是一个例外,用于指示何时查找员工但未找到该员工。
package payroll;
class EmployeeNotFoundException extends RuntimeException {
EmployeeNotFoundException(Long id) {
super("Could not find employee " + id);
}
}
EmployeeNotFoundException
抛出an时,Spring MVC配置的这个额外花絮用于呈现HTTP 404:
package payroll;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
class EmployeeNotFoundAdvice {
@ResponseBody
@ExceptionHandler(EmployeeNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
String employeeNotFoundHandler(EmployeeNotFoundException ex) {
return ex.getMessage();
}
}
-
@ResponseBody
表示此建议已直接呈现到响应主体中。 -
@ExceptionHandler
将建议配置为仅EmployeeNotFoundException
在抛出an时做出响应。 -
@ResponseStatus
表示发出一个HttpStatus.NOT_FOUND
,即HTTP 404。 -
建议的正文生成内容。在这种情况下,它会给出异常消息。
要启动该应用程序,请右键单击其中public static void main
的PayRollApplication
并从IDE中选择“运行”,或者:
Spring Initializr使用Maven包装器,因此键入:
$ ./mvnw clean spring-boot:run
或者使用您安装的Maven版本键入以下命令:
$ mvn clean spring-boot:运行
应用启动后,我们可以立即对其进行查询。
$ curl -v本地主机:8080 /员工
这将产生:
*正在尝试:: 1 ... * TCP_NODELAY设置 *连接到localhost(:: 1)端口8080(#0) > GET /员工HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * > <HTTP / 1.1 200 <内容类型:application / json; charset = UTF-8 <传输编码:分块 <日期:2018年8月9日,星期四,格林尼治标准时间 < *与主机localhost的连接#0完好无损 [{{id“:1,” name“:” Bilbo Baggins“,” role“:”防盗“},{” id“:2,” name“:” Frodo Baggins“,” role“:” thief“} ]
在这里,您可以以压缩格式查看预加载的数据。
如果您尝试查询一个不存在的用户...
$ curl -v本地主机:8080 / employees / 99
你得到...
*正在尝试:: 1 ... * TCP_NODELAY设置 *连接到localhost(:: 1)端口8080(#0) > GET /员工/ 99 HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * > <HTTP / 1.1 404 <内容类型:文本/纯文本;字符集= UTF-8 <内容长度:26 <日期:2018年8月9日,星期四,格林尼治标准时间 < *与主机localhost的连接#0完好无损 找不到员工99
此消息很好地显示了HTTP 404错误和自定义消息“找不到雇员99”。
显示当前编码的交互并不难...
如果您使用Windows命令提示符发出cURL命令,则以下命令可能无法正常工作。您必须选择一个支持单引号引号的终端,或者使用双引号然后将其转义为JSON。 |
要创建新Employee
记录,我们在终端中使用以下命令-$
开头表示其后是终端命令:
$ curl -X POST localhost:8080 / employees -H'Content-type:application / json'-d'{“ name”:“ Samwise Gamgee”,“ role”:“ gardener”}'
然后,它将存储新创建的员工并将其发送回给我们:
{“ id”:3,“ name”:“ Samwise Gamgee”,“ role”:“ gardener”}
您可以更新用户。让我们改变他的角色。
$ curl -X PUT本地主机:8080 / employees / 3 -H'Content-type:application / json'-d'{“ name”:“ Samwise Gamgee”,“ role”:“ ring bearer”}'
我们可以看到更改反映在输出中。
{“ id”:3,“ name”:“ Samwise Gamgee”,“ role”:“ ring bearer”}
构建服务的方式可能会产生重大影响。在这种情况下,我们说过update,但是replace是一个更好的描述。例如,如果未提供名称,则将其清空。 |
最后,您可以像这样删除用户:
$ curl -X DELETE本地主机:8080 / employees / 3 #现在,如果我们再看一次,它就消失了 $ curl本地主机:8080 / employees / 3 找不到员工3
这一切都很好,但是我们有RESTful服务吗?(如果您没有收到提示,那么答案是否定的。)
缺少了什么?
是什么使RESTful变得有趣?
到目前为止,您已经有了基于Web的服务,该服务可以处理涉及员工数据的核心操作。但这还不足以使事情变得“ RESTful”。
-
漂亮的URL
/employees/3
并非REST。 -
仅使用
GET
,POST
等不是REST。 -
安排所有CRUD操作不是REST。
实际上,到目前为止,我们更好地描述了RPC(远程过程调用)。那是因为没有办法知道如何与该服务进行交互。如果您今天发布了此文档,则还必须编写文档或将开发人员的门户托管在所有详细信息的某个位置。
Roy Fielding的这一声明可能进一步为REST和RPC之间的区别提供了线索:
人们对将任何基于HTTP的接口称为REST API的人数感到沮丧。今天的示例是SocialSite REST API。那就是RPC。它尖叫RPC。显示器上耦合太多,因此应给定X等级。
要使REST体系结构风格清晰地认识到超文本是一种约束,需要采取什么措施?换句话说,如果应用程序状态的引擎(以及API)不是由超文本驱动的,则它不能是RESTful的,也不能是REST API。时期。是否有一些需要修复的损坏的手册?
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-Hypertext-driven
在我们的表示中不包含超媒体的副作用是客户端必须使用硬编码URI来导航API。这导致了与电子商务在网络上兴起之前一样的脆弱性。这表明我们的JSON输出需要一点帮助。
介绍Spring HATEOAS,这是一个Spring项目,旨在帮助您编写超媒体驱动的输出。要将服务升级为RESTful,请将其添加到您的构建中:
dependencies
第pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
这个很小的库将为我们提供构造以定义RESTful服务,然后将其呈现为可接受的格式以供客户端使用。
任何RESTful服务的关键要素是添加到相关操作的链接。为了使您的控制器更加RESTful,请添加如下链接:
@GetMapping("/employees/{id}")
EntityModel<Employee> one(@PathVariable Long id) {
Employee employee = repository.findById(id) //
.orElseThrow(() -> new EmployeeNotFoundException(id));
return EntityModel.of(employee, //
linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));
}
本教程基于Spring MVC,并使用from中的静态帮助器方法 |
这与我们以前的非常相似,但是有一些变化:
-
方法的返回类型已从更改
Employee
为EntityModel<Employee>
。EntityModel<T>
是Spring HATEOAS的通用容器,它不仅包含数据,还包含链接的集合。 -
linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()
要求Spring HATEOAS建立到EmployeeController
的one()
方法的链接,并将其标记为自我链接。 -
linkTo(methodOn(EmployeeController.class).all()).withRel("employees")
要求Spring HATEOAS构建到聚合根的链接all()
,并将其称为“雇员”。
“建立链接”是什么意思?Spring HATEOAS的核心类型之一是Link
。它包括一个URI和一个rel(关系)。链接是赋予网络权力的要素。在万维网出现之前,其他文档系统会呈现信息或链接,但是将文档与具有这种关系元数据的链接紧密地联系在一起就是网络。
Roy Fielding鼓励使用使网络成功的相同技术来构建API,链接就是其中之一。
如果重新启动应用程序并查询Bilbo的雇员记录,您将得到与之前稍有不同的响应:
冰壶漂亮
#指示的部分将输出通过管道传递到json_pp,并要求其使JSON变得漂亮。(或使用任何您喜欢的工具!) #v ------------------ v curl -v本地主机:8080 / employees / 1 | json_pp |
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
这个解压后的输出不仅说明你前面看到(数据元素id
,name
并role
),也_links
包含两个URI条目。整个文档使用HAL格式化。
为了使聚合根ALSO更具RESTful,您希望包括顶级链接,同时ALSO包括其中的所有RESTful组件。
所以我们把这个
@GetMapping("/employees")
List<Employee> all() {
return repository.findAll();
}
进入这个
@GetMapping("/employees")
CollectionModel<EntityModel<Employee>> all() {
List<EntityModel<Employee>> employees = repository.findAll().stream()
.map(employee -> EntityModel.of(employee,
linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees")))
.collect(Collectors.toList());
return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
}
哇!那曾经是的方法repository.findAll()
已经长大了!不用担心。让我们打开包装。
CollectionModel<>
是另一个Spring HATEOAS容器;它旨在封装资源集合,而不是像EntityModel<>
以前那样封装单个资源实体。CollectionModel<>
,也允许您包含链接。
不要让第一个陈述漏掉。“封装集合”是什么意思?员工收款?
不完全的。
由于我们在谈论REST,因此它应该封装员工资源的集合。
这就是为什么要获取所有员工,然后将其转换为EntityModel<Employee>
对象列表的原因。(感谢Java 8流!)
如果重新启动应用程序并获取聚合根,则现在可以看到它的外观。
{
"_embedded": {
"employeeList": [
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
},
{
"id": 2,
"name": "Frodo Baggins",
"role": "thief",
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/employees"
}
}
}
对于服务于员工资源集合的聚合根,有一个顶层“自我”链接。在“收藏”被列在下面“_embedded”部分; 这就是HAL表示集合的方式。
并且集合中的每个成员都有其信息以及相关链接。
添加所有这些链接的意义何在?随着时间的推移,它使发展REST服务成为可能。可以维护现有链接,而将来可以添加新链接。较新的客户端可以利用新链接,而旧客户端可以在旧链接上维持自己的状态。如果服务被重新定位和移动,这将特别有用。只要保持链接结构,客户端就可以查找并与事物进行交互。
简化链接创建
在前面的代码中,您是否注意到在创建单个员工链接时重复执行此操作?两次显示了提供指向员工的单个链接以及创建指向聚合根的“员工”链接的代码。如果那引起您的关注,那就好!有一个解决方案。
简而言之,您需要定义一个将Employee
对象转换为EntityModel<Employee>
对象的函数。尽管您可以轻松地自己编写此方法,但是在实现Spring HATEOASRepresentationModelAssembler
接口的过程中会受益匪浅-它将为您完成工作。
package payroll;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;
@Component
class EmployeeModelAssembler implements RepresentationModelAssembler<Employee, EntityModel<Employee>> {
@Override
public EntityModel<Employee> toModel(Employee employee) {
return EntityModel.of(employee, //
linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));
}
}
这个简单的界面有一种方法:toModel()
。它基于将非模型对象(Employee
)转换为基于模型的对象(EntityModel<Employee>
)。
您之前在控制器中看到的所有代码都可以移入此类。通过应用Spring Framework的@Component
注释,将在应用启动时自动创建汇编器。
Spring HATEOAS所有模型的抽象基类是RepresentationModel 。但是为简单起见,我建议使用EntityModel<T> 作为机制轻松地将所有POJO包装为模型。 |
要利用此汇编程序,您只需EmployeeController
通过将汇编程序注入到构造函数中来更改即可。
@RestController
class EmployeeController {
private final EmployeeRepository repository;
private final EmployeeModelAssembler assembler;
EmployeeController(EmployeeRepository repository, EmployeeModelAssembler assembler) {
this.repository = repository;
this.assembler = assembler;
}
...
}
从这里,您可以在单项employee方法中使用该汇编器:
@GetMapping("/employees/{id}")
EntityModel<Employee> one(@PathVariable Long id) {
Employee employee = repository.findById(id) //
.orElseThrow(() -> new EmployeeNotFoundException(id));
return assembler.toModel(employee);
}
这段代码几乎相同,除了EntityModel<Employee>
将其委托给汇编器之外,而不是在此处创建实例。也许看起来并不多。
在聚合根控制器方法中应用相同的内容会更加令人印象深刻:
@GetMapping("/employees")
CollectionModel<EntityModel<Employee>> all() {
List<EntityModel<Employee>> employees = repository.findAll().stream() //
.map(assembler::toModel) //
.collect(Collectors.toList());
return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
}
再次,代码几乎是相同的,但是您可以将所有EntityModel<Employee>
创建逻辑替换为map(assembler::toModel)
。感谢Java 8方法参考,将其插入并简化您的控制器非常容易。
Spring HATEOAS的主要设计目标是使“正确的事情”变得更容易。在这种情况下:在不对事物进行硬编码的情况下将超媒体添加到服务中。 |
在此阶段,您已经创建了一个Spring MVC REST控制器,该控制器实际上可以生成超媒体支持的内容!不使用HAL的客户端在使用纯数据时可以忽略多余的位。会说HAL的客户端可以浏览您的授权API。
但这不是用Spring构建真正的RESTful服务所需要的唯一东西。
不断发展的REST API
使用一个附加的库和几行附加的代码,您已将超媒体添加到您的应用程序中。但这不是使服务成为RESTful所需的唯一条件。REST的一个重要方面是它既不是技术堆栈也不是单一标准。
REST是体系结构约束的集合,采用这些约束可使您的应用程序更具弹性。弹性的关键因素是,当您升级服务时,您的客户不会遭受停机时间的困扰。
在“过去”的日子里,升级是臭名昭著的,因为它破坏了客户。换句话说,对服务器的升级需要对客户端的更新。在当今时代,升级花费的停机时间甚至数小时甚至数分钟可能会导致数百万美元的收入损失。
一些公司要求您向管理层提出计划,以最大程度地减少停机时间。过去,您可以在周日凌晨2:00进行升级,而此时负载已降至最低。但是,在当今与其他时区的国际客户进行的基于Internet的电子商务中,这种策略并不那么有效。
支持对API的更改
想象一下这个设计问题:您已经推出了一个Employee
基于此记录的系统。该系统是一个重大打击。您已将系统卖给了无数企业。突然,需要对员工的名字被分成firstName
和lastName
产生。
哦哦 没想到。
之前你打开Employee
类和替换单场name
同firstName
和lastName
,停下来思考了一秒钟。这样会打断任何客户吗?升级它们需要多长时间。您甚至控制所有访问您服务的客户端吗?
停机时间=赔钱。管理层为此做好了准备吗?
有一种古老的策略要比REST早很多年。
切勿删除数据库中的列。
您始终可以将列(字段)添加到数据库表中。但是不要带走一个。RESTful服务的原理是相同的。
向您的JSON表示中添加新字段,但不要花任何时间。像这样:
{
"id": 1,
"firstName": "Bilbo",
"lastName": "Baggins",
"role": "burglar",
"name": "Bilbo Baggins",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
请注意如何体现这种格式firstName
,lastName
,和name
?它具有重复信息的功能,目的是为新老客户提供支持。这意味着您可以升级服务器而无需同时升级客户端。一个很好的举动应该可以减少停机时间。
而且,您不仅应该以“旧方式”和“新方式”显示此信息,还应该以两种方式处理传入的数据。
如何?简单的。像这样:
package payroll;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String role;
Employee() {}
Employee(String firstName, String lastName, String role) {
this.firstName = firstName;
this.lastName = lastName;
this.role = role;
}
public String getName() {
return this.firstName + " " + this.lastName;
}
public void setName(String name) {
String[] parts = name.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
public Long getId() {
return this.id;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public String getRole() {
return this.role;
}
public void setId(Long id) {
this.id = id;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setRole(String role) {
this.role = role;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Employee))
return false;
Employee employee = (Employee) o;
return Objects.equals(this.id, employee.id) && Objects.equals(this.firstName, employee.firstName)
&& Objects.equals(this.lastName, employee.lastName) && Objects.equals(this.role, employee.role);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.firstName, this.lastName, this.role);
}
@Override
public String toString() {
return "Employee{" + "id=" + this.id + ", firstName='" + this.firstName + '\'' + ", lastName='" + this.lastName
+ '\'' + ", role='" + this.role + '\'' + '}';
}
}
此类与的早期版本非常相似Employee
。让我们来看一下更改:
-
字段
name
已被firstName
和代替lastName
。 -
定义了旧
name
属性的“虚拟”吸气剂getName()
。它使用firstName
和lastName
字段产生一个值。 -
name
还定义了旧属性的“虚拟”设置器setName()
。它解析输入的字符串并将其存储到适当的字段中。
当然,对您的API所做的每一次更改都不像拆分字符串或合并两个字符串那样简单。但是,对于大多数情况,一定要想出一套转换方法,对吗?
不要忘记去更改如何预加载数据库(在中
|
适当的回应
朝着正确方向迈出的另一步涉及确保您的每个REST方法都返回正确的响应。像这样更新POST方法:
@PostMapping("/employees")
ResponseEntity<?> newEmployee(@RequestBody Employee newEmployee) {
EntityModel<Employee> entityModel = assembler.toModel(repository.save(newEmployee));
return ResponseEntity //
.created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) //
.body(entityModel);
}
-
新
Employee
对象将像以前一样保存。但是结果对象使用包裹EmployeeModelAssembler
。 -
Spring MVC
ResponseEntity
用于创建HTTP 201已创建状态消息。这种类型的响应通常包括一个Location响应标头,并且我们使用从模型的自相关链接派生的URI。 -
此外,返回已保存对象的基于模型的版本。
完成这些调整后,您可以使用相同的端点来创建新的员工资源,并使用legacyname
字段:
$ curl -v -X POST localhost:8080 / employees -H'Content-Type:application / json'-d'{“ name”:“ Samwise Gamgee”,“ role”:“ gardener”}'
输出如下所示:
> POST /员工HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * >内容类型:application / json >内容长度:46 > <位置:http:// localhost:8080 / employees / 3 <内容类型:application / hal + json; charset = UTF-8 <传输编码:分块 <日期:2018年8月10日星期五19:44:43 GMT < { “ id”:3, “ firstName”:“ Samwise”, “ lastName”:“ Gamgee”, “ role”:“ gardener”, “ name”:“ Samwise Gamgee”, “ _links”:{ “自己”: { “ href”:“ http:// localhost:8080 / employees / 3” }, “雇员”: { “ href”:“ http:// localhost:8080 / employees” } } }
这不仅使生成的对象以HAL形式(name
以及firstName
/ lastName
)呈现,而且还使用填充了Location标头http://localhost:8080/employees/3
。具有超媒体功能的客户端可以选择“浏览”该新资源并继续与之交互。
PUT控制器方法需要类似的调整:
@PutMapping("/employees/{id}")
ResponseEntity<?> replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) {
Employee updatedEmployee = repository.findById(id) //
.map(employee -> {
employee.setName(newEmployee.getName());
employee.setRole(newEmployee.getRole());
return repository.save(employee);
}) //
.orElseGet(() -> {
newEmployee.setId(id);
return repository.save(newEmployee);
});
EntityModel<Employee> entityModel = assembler.toModel(updatedEmployee);
return ResponseEntity //
.created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) //
.body(entityModel);
}
然后,使用操作将通过操作Employee
构建的对象save()
包装EmployeeModelAssembler
到一个EntityModel<Employee>
对象中。使用getRequiredLink()
方法,你可以检索Link
通过创建EmployeeModelAssembler
一个SELF
版本。该方法返回一个Link
必须被变成URI
与toUri
方法。
由于我们需要比200 OK更详细的HTTP响应代码,因此我们将使用Spring MVC的ResponseEntity
包装器。它有一个方便的静态方法created()
,我们可以在其中插入资源的URI。如果HTTP 201 Created带有正确的语义,这是有争议的,因为我们不一定要“创建”新资源。但是它预装了Location响应标头,因此请与它一起运行。
$ curl -v -X PUT本地主机:8080 / employees / 3 -H'Content-Type:application / json'-d'{“ name”:“ Samwise Gamgee”,“ role”:“ ring bearer”}' * TCP_NODELAY设置 *连接到localhost(:: 1)端口8080(#0) > PUT /员工/ 3 HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * >内容类型:application / json >内容长度:49 > <HTTP / 1.1 201 <位置:http:// localhost:8080 / employees / 3 <内容类型:application / hal + json; charset = UTF-8 <传输编码:分块 <日期:2018年8月10日星期五19:52:56 GMT { “ id”:3, “ firstName”:“ Samwise”, “ lastName”:“ Gamgee”, “ role”:“ ring bearer”, “ name”:“ Samwise Gamgee”, “ _links”:{ “自己”: { “ href”:“ http:// localhost:8080 / employees / 3” }, “雇员”: { “ href”:“ http:// localhost:8080 / employees” } } }
该员工资源现已更新,并且位置URI被发送回。最后,适当地更新DELETE操作:
@DeleteMapping("/employees/{id}")
ResponseEntity<?> deleteEmployee(@PathVariable Long id) {
repository.deleteById(id);
return ResponseEntity.noContent().build();
}
这将返回HTTP 204 No Content响应。
$ curl -v -X删除本地主机:8080 / employees / 1 * TCP_NODELAY设置 *连接到localhost(:: 1)端口8080(#0) >删除/ employees / 1 HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * > <HTTP / 1.1 204 <日期:2018年8月10日星期五21:30:26 GMT
更改类中的字段Employee 将需要与您的数据库团队协调,以便他们可以将现有内容正确迁移到新列中。 |
现在,您可以进行升级了,它不会打扰现有的客户端,而新的客户端可以利用这些增强功能!
顺便说一句,您是否担心通过网络发送太多信息?在某些每个字节都很重要的系统中,API的发展可能需要退居二线。但是,在进行测量之前,不要追求这种过早的优化。
将链接构建到您的REST API中
到目前为止,您已经建立了具有裸露骨骼链接的可演化API。为了增加您的API并更好地为您的客户服务,您需要接受Hypermedia作为应用程序状态引擎的概念。
这意味着什么?在本节中,您将详细研究它。
业务逻辑不可避免地建立涉及流程的规则。此类系统的风险在于,我们经常将此类服务器端逻辑带入客户端,并建立牢固的耦合。REST旨在断开此类连接并最大程度地减少这种耦合。
为了说明如何在不触发客户端变更的情况下应对状态变化,请设想添加一个可以执行订单的系统。
第一步,定义一条Order
记录:
package payroll;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "CUSTOMER_ORDER")
class Order {
private @Id @GeneratedValue Long id;
private String description;
private Status status;
Order() {}
Order(String description, Status status) {
this.description = description;
this.status = status;
}
public Long getId() {
return this.id;
}
public String getDescription() {
return this.description;
}
public Status getStatus() {
return this.status;
}
public void setId(Long id) {
this.id = id;
}
public void setDescription(String description) {
this.description = description;
}
public void setStatus(Status status) {
this.status = status;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Order))
return false;
Order order = (Order) o;
return Objects.equals(this.id, order.id) && Objects.equals(this.description, order.description)
&& this.status == order.status;
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.description, this.status);
}
@Override
public String toString() {
return "Order{" + "id=" + this.id + ", description='" + this.description + '\'' + ", status=" + this.status + '}';
}
}
-
该类需要一个JPA
@Table
批注,将表名更改为,CUSTOMER_ORDER
因为ORDER
这不是表的有效名称。 -
它包括一个
description
字段以及一个status
字段。
从客户提交订单到完成或取消订单之时,订单必须经历一系列特定的状态转换。可以将其捕获为Java enum
:
package payroll;
enum Status {
IN_PROGRESS, //
COMPLETED, //
CANCELLED
}
这enum
捕获了一个Order
罐可以占据的各种状态。对于本教程,让我们保持简单。
为了支持与数据库中的订单进行交互,您必须定义一个相应的Spring Data存储库:
JpaRepository
基本接口
interface OrderRepository extends JpaRepository<Order, Long> {
}
有了这个,您现在可以定义一个基本的OrderController
:
@RestController
class OrderController {
private final OrderRepository orderRepository;
private final OrderModelAssembler assembler;
OrderController(OrderRepository orderRepository, OrderModelAssembler assembler) {
this.orderRepository = orderRepository;
this.assembler = assembler;
}
@GetMapping("/orders")
CollectionModel<EntityModel<Order>> all() {
List<EntityModel<Order>> orders = orderRepository.findAll().stream() //
.map(assembler::toModel) //
.collect(Collectors.toList());
return CollectionModel.of(orders, //
linkTo(methodOn(OrderController.class).all()).withSelfRel());
}
@GetMapping("/orders/{id}")
EntityModel<Order> one(@PathVariable Long id) {
Order order = orderRepository.findById(id) //
.orElseThrow(() -> new OrderNotFoundException(id));
return assembler.toModel(order);
}
@PostMapping("/orders")
ResponseEntity<EntityModel<Order>> newOrder(@RequestBody Order order) {
order.setStatus(Status.IN_PROGRESS);
Order newOrder = orderRepository.save(order);
return ResponseEntity //
.created(linkTo(methodOn(OrderController.class).one(newOrder.getId())).toUri()) //
.body(assembler.toModel(newOrder));
}
}
-
它包含与到目前为止构建的控制器相同的REST控制器设置。
-
它同时注入
OrderRepository
和(尚未构建)OrderModelAssembler
。 -
前两个Spring MVC路由处理聚合根以及单个项目
Order
资源请求。 -
第三条Spring MVC路线通过在
IN_PROGRESS
状态中启动新订单来处理创建新订单。 -
所有控制器方法都将返回Spring HATEOAS的
RepresentationModel
子类之一,以正确呈现超媒体(或围绕此类的包装器)。
在构建之前OrderModelAssembler
,让我们讨论需要发生的事情。你模拟状态之间的流动Status.IN_PROGRESS
,Status.COMPLETED
和Status.CANCELLED
。向客户端提供此类数据时,很自然的事情是让客户端根据此有效负载决定它可以做什么。
但这是错误的。
当您在此流程中引入新状态时会发生什么?UI上各种按钮的放置可能是错误的。
如果您更改了每个州的名称,可能是在编码国际支持并显示每个州的特定于语言环境的文本时呢?那很可能会破坏所有客户。
输入HATEOAS或Hypermedia作为应用程序状态引擎。与其让客户端解析有效载荷,不如让客户端链接以发出有效动作的信号。将基于状态的操作与数据的有效负载分离。换句话说,当CANCEL和COMPLETE是有效动作时,将它们动态添加到链接列表中。链接存在时,客户端仅需要向用户显示相应的按钮。
这使客户端不必知道何时需要执行此类操作,从而减少了服务器及其客户端在状态转换逻辑上不同步的风险。
已经接受了Spring HATEOASRepresentationModelAssembler
组件的概念,将这样的逻辑放进去OrderModelAssembler
将是捕获此业务规则的理想场所:
package payroll;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;
@Component
class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<Order>> {
@Override
public EntityModel<Order> toModel(Order order) {
// Unconditional links to single-item resource and aggregate root
EntityModel<Order> orderModel = EntityModel.of(order,
linkTo(methodOn(OrderController.class).one(order.getId())).withSelfRel(),
linkTo(methodOn(OrderController.class).all()).withRel("orders"));
// Conditional links based on state of the order
if (order.getStatus() == Status.IN_PROGRESS) {
orderModel.add(linkTo(methodOn(OrderController.class).cancel(order.getId())).withRel("cancel"));
orderModel.add(linkTo(methodOn(OrderController.class).complete(order.getId())).withRel("complete"));
}
return orderModel;
}
}
该资源汇编器始终包括指向单项资源的自身链接以及指向聚合根的链接。但是它还包括和的两个条件链接OrderController.cancel(id)
以及OrderController.complete(id)
(尚未定义)。仅当订单状态为时,才会显示这些链接Status.IN_PROGRESS
。
如果客户可以采用HAL并具有读取链接的能力,而不是简单地读取普通的旧JSON数据,则可以交易对订单系统领域知识的需求。这自然减少了客户端和服务器之间的耦合。它为调整订单履行流程打开了一扇门,而不会破坏流程中的客户。
为了完善订单执行,下面添加到OrderController
的cancel
操作:
@DeleteMapping("/orders/{id}/cancel")
ResponseEntity<?> cancel(@PathVariable Long id) {
Order order = orderRepository.findById(id) //
.orElseThrow(() -> new OrderNotFoundException(id));
if (order.getStatus() == Status.IN_PROGRESS) {
order.setStatus(Status.CANCELLED);
return ResponseEntity.ok(assembler.toModel(orderRepository.save(order)));
}
return ResponseEntity //
.status(HttpStatus.METHOD_NOT_ALLOWED) //
.header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) //
.body(Problem.create() //
.withTitle("Method not allowed") //
.withDetail("You can't cancel an order that is in the " + order.getStatus() + " status"));
}
它会Order
在允许取消之前检查状态。如果状态无效,则返回RFC-7807 Problem
(支持超媒体的错误容器)。如果转换确实有效,则将转换Order
为CANCELLED
。
并将其添加到OrderController
中以完成订单:
@PutMapping("/orders/{id}/complete")
ResponseEntity<?> complete(@PathVariable Long id) {
Order order = orderRepository.findById(id) //
.orElseThrow(() -> new OrderNotFoundException(id));
if (order.getStatus() == Status.IN_PROGRESS) {
order.setStatus(Status.COMPLETED);
return ResponseEntity.ok(assembler.toModel(orderRepository.save(order)));
}
return ResponseEntity //
.status(HttpStatus.METHOD_NOT_ALLOWED) //
.header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) //
.body(Problem.create() //
.withTitle("Method not allowed") //
.withDetail("You can't complete an order that is in the " + order.getStatus() + " status"));
}
这实现了类似的逻辑,以防止Order
除非处于适当的状态,否则无法完成状态。
让我们进行更新LoadDatabase
以预加载某些内容Order
以及Employee
之前加载的内容。
package payroll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class LoadDatabase {
private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);
@Bean
CommandLineRunner initDatabase(EmployeeRepository employeeRepository, OrderRepository orderRepository) {
return args -> {
employeeRepository.save(new Employee("Bilbo", "Baggins", "burglar"));
employeeRepository.save(new Employee("Frodo", "Baggins", "thief"));
employeeRepository.findAll().forEach(employee -> log.info("Preloaded " + employee));
orderRepository.save(new Order("MacBook Pro", Status.COMPLETED));
orderRepository.save(new Order("iPhone", Status.IN_PROGRESS));
orderRepository.findAll().forEach(order -> {
log.info("Preloaded " + order);
});
};
}
}
现在您可以测试了!
要使用新创建的订单服务,只需执行一些操作:
$ curl -v http://本地主机:8080 / orders { “ _embedded”:{ “订单”: [ { “ id”:3, “ description”:“ MacBook Pro”, “ status”:“ COMPLETED”, “ _links”:{ “自己”: { “ href”:“ http:// localhost:8080 / orders / 3” }, “命令”: { “ href”:“ http:// localhost:8080 / orders” } } }, { “ id”:4 “ description”:“ iPhone”, “状态”:“ IN_PROGRESS”, “ _links”:{ “自己”: { “ href”:“ http:// localhost:8080 / orders / 4” }, “命令”: { “ href”:“ http:// localhost:8080 / orders” }, “取消”: { “ href”:“ http:// localhost:8080 / orders / 4 / cancel” }, “完全的”: { “ href”:“ http:// localhost:8080 / orders / 4 / complete” } } } ] }, “ _links”:{ “自己”: { “ href”:“ http:// localhost:8080 / orders” } } }
该HAL文档根据其当前状态立即显示每个订单的不同链接。
-
一阶,被已完成只有导航链接。状态转换链接未显示。
-
第二个订单(IN_PROGRESS)另外具有取消链接和完整链接。
尝试取消订单:
$ curl -v -X删除http:// localhost:8080 / orders / 4 / cancel >删除/ orders / 4 /取消HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * > <HTTP / 1.1 200 <内容类型:application / hal + json; charset = UTF-8 <传输编码:分块 <日期:2018年8月27日星期一15:02:10 GMT < { “ id”:4 “ description”:“ iPhone”, “状态”:“已取消”, “ _links”:{ “自己”: { “ href”:“ http:// localhost:8080 / orders / 4” }, “命令”: { “ href”:“ http:// localhost:8080 / orders” } } }
此响应显示指示成功的HTTP 200状态代码。响应HAL文档以新状态(CANCELLED
)显示该顺序。改变状态的链接也消失了。
如果您再次尝试相同的操作...
$ curl -v -X删除http:// localhost:8080 / orders / 4 / cancel * TCP_NODELAY设置 *连接到localhost(:: 1)端口8080(#0) >删除/ orders / 4 /取消HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * > <HTTP / 1.1 405 <Content-Type:应用程序/问题+ json <传输编码:分块 <日期:2018年8月27日星期一15:03:24 GMT < { “ title”:“不允许使用方法”, “ detail”:“您无法取消处于CANCELED状态的订单” }
......您会看到HTTP 405方法不允许响应。删除已成为无效操作。该Problem
响应对象清楚地表明,你不准“取消”,在“取消”状态已经下订单。
此外,尝试完成相同的订单也会失败:
$ curl -v -X PUT本地主机:8080 / orders / 4 / complete * TCP_NODELAY设置 *连接到localhost(:: 1)端口8080(#0) > PUT /订单/ 4 /完整的HTTP / 1.1 >主机:localhost:8080 >用户代理:curl / 7.54.0 >接受:* / * > <HTTP / 1.1 405 <Content-Type:应用程序/问题+ json <传输编码:分块 <日期:2018年8月27日星期一15:05:40 GMT < { “ title”:“不允许使用方法”, “ detail”:“您无法完成处于CANCELED状态的订单” }
完成所有这些操作后,您的订单履行服务便可以有条件地显示可用的操作。它还可以防止无效操作。
通过利用超媒体和链接协议,可以使客户端更坚固,并且仅因数据更改而导致崩溃的可能性较小。Spring HATEOAS简化了构建为客户服务所需的超媒体的过程。
概括
在本教程中,您参与了各种构建REST API的策略。事实证明,REST不仅涉及漂亮的URI,而且还返回JSON而不是XML。
相反,以下策略有助于使您的服务不太可能破坏您可能控制或可能无法控制的现有客户端:
-
不要删除旧字段。相反,支持他们。
-
使用基于rel的链接,这样客户端就不必对URI进行硬编码。
-
尽可能保留旧的链接。即使必须更改URI,也请保留rels,以便较旧的客户端可以使用较新的功能。
-
使用链接(而不是有效负载数据)来指示客户端何时可以进行各种状态驱动操作。
RepresentationModelAssembler
为每种资源类型建立实现并在所有控制器中使用这些组件可能看起来有些努力。但是,服务器端设置的这一额外点(借助Spring HATEOAS可以轻松实现)可以确保您控制的客户端(更重要的是,那些您不需要的客户端)可以在您开发API时轻松升级。
到此,我们的教程结束了如何使用Spring构建RESTful服务。本教程的每个部分在单个github存储库中作为单独的子项目进行管理:
-
nonrest —没有超媒体的简单Spring MVC应用程序
-
rest — Spring MVC + Spring HATEOAS应用程序,每个资源都有HAL表示形式
-
演化— REST应用程序,其中的字段已演化,但保留了旧数据以实现向后兼容性
-
链接-REST应用程序,其中使用条件链接向客户端发送有效状态更改的信号
要查看使用Spring HATEOAS的更多示例,请参见https://github.com/spring-projects/spring-hateoas-examples。
要进行更多探索,请查看Spring队友Oliver Gierke的以下视频:
是否要编写新指南或为现有指南做出贡献?查看我们的贡献准则。
所有指南均以代码的ASLv2许可证和写作的Attribution,NoDerivatives创用CC许可证发布。 |