使用 Spring 构建 REST 服务

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 Spring 构建 REST 服务相关的知识,希望对你有一定的参考价值。

使用

REST 已迅速成为在 Web 上构建 Web 服务的事实标准,因为它们易于构建和使用。

关于 REST 如何适应微服务领域,还有更大的讨论,但是在本教程中,让我们只看一下构建 RESTful 服务。

为什么休息?REST 包含 Web 的准则,包括其架构、优势和其他一切。这并不奇怪,因为它的作者罗伊·菲尔丁(Roy Fielding)参与了大约十几个规范,这些规范控制着网络的运作方式。

有什么好处?Web及其核心协议HTTP提供了一系列功能:

  • 适当的操作(、、、、...)GETPOSTPUTDELETE
  • 缓存
  • 重定向和转发
  • 安全性(加密和身份验证)

这些都是构建弹性服务的关键因素。但这还不是全部。网络是由许多微小的规范构建而成的,因此它能够轻松发展,而不会陷入“标准战争”。

开发人员能够利用实现这些不同规范的第三方工具包,并立即拥有触手可及的客户端和服务器技术。

通过在HTTP之上构建,REST API提供了构建的方法:

  • 向后兼容的 API
  • 可进化的接口
  • 可扩展的服务
  • 安全对象服务
  • 从无状态到有状态服务的一系列

重要的是要认识到,REST,无论多么普遍,本身并不是一个标准,而是一种方法,一种风格,一组对架构的约束,可以帮助你构建Web规模的系统。在本教程中,我们将使用 Spring 产品组合来构建 RESTful 服务,同时利用 REST 的无堆栈功能。

开始

在完成本教程时,我们将使用弹簧启动.转到Spring Initializr并将以下依赖项添加到项目中:

  • 太平绅士
  • H2

将名称更改为“工资单”,然后选择“生成项目”。A 将下载。解压缩它。在里面你会发现一个简单的,基于Maven的项目,包括一个构建文件(注意:你可以使用Gradle。本教程中的示例将基于 Maven。​​.zip​​​​pom.xml​

Spring Boot 可以与任何 IDE 一起使用。您可以使用Eclipse,IntelliJ IDEA,Netbeans等。弹簧工具套件是一个开源的、基于 Eclipse 的 IDE 发行版,它提供了 Eclipse 的 Java EE 发行版的超集。它包括使使用 Spring 应用程序更加容易的功能。这绝不是必需的。但是,如果您想为击键提供额外的魅力,请考虑一下。

到目前为止的故事...

让我们从我们可以构建的最简单的东西开始。事实上,为了使它尽可能简单,我们甚至可以省略REST的概念。 (稍后,我们将添加REST来理解其中的区别。

大图:我们将创建一个简单的工资单服务来管理公司的员工。我们将员工对象存储在(H2 内存中)数据库中,并访问它们(通过称为 JPA 的东西)。然后,我们将用允许通过互联网访问的东西(称为Spring MVC层)来包装它。

以下代码在我们的系统中定义了一个员工。

nonrest/src/main/java/payroll/Employee.java

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​​、 和 是我们员工的属性namerole域对象. 用更多 JPA 注释标记,以指示它是主键,并由 JPA 提供程序自动填充。id
  • 当我们需要创建新实例但还没有 ID 时,会创建自定义构造函数。

有了这个域对象定义,我们现在可以转向春季数据 JPA处理繁琐的数据库交互。

Spring Data JPA 存储库是具有支持针对后端数据存储创建、读取、更新和删除记录的方法的接口。某些存储库还支持数据分页和排序(如果适用)。Spring Data 根据接口中方法命名中的约定合成实现。

除了 JPA 之外,还有多个存储库实现。您可以使用Spring Data MongoDB,Spring Data GemFire,Spring Data Cassandra等。在本教程中,我们将坚持使用 JPA。

Spring 使访问数据变得容易。通过简单地声明以下接口,我们将能够自动​​EmployeeRepository​

  • 创建新员工
  • 更新现有
  • 删除员工
  • 查找员工(一个、全部或按简单或复杂属性搜索)

nonrest/src/main/java/payroll/EmployeeRepository.java

package payroll;

import org.springframework.data.jpa.repository.JpaRepository;

interface EmployeeRepository extends JpaRepository<Employee, Long>

为了获得所有这些免费功能,我们所要做的就是声明一个扩展Spring Data JPA的接口,将域类型指定为,将id类型指定为。​​JpaRepository​​​​Employee​​​​Long​

春季数据存储库解决方案可以避开数据存储细节,而是使用特定于域的术语解决大多数问题。

信不信由你,这足以启动一个应用程序!Spring 引导应用程序至少是一个入口点和注释。这告诉Spring Boot尽可能提供帮助。​​public static void main​​​​@SpringBootApplication​

nonrest/src/main/java/payroll/PayrollApplication.java

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 容器并提供我们的服务。

尽管如此,没有数据的应用程序并不是很有趣,所以让我们预加载它。以下类将由 Spring 自动加载:

nonrest/src/main/java/payroll/LoadDatabase.java

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")));
;

加载时会发生什么?

  • 一旦加载了应用程序上下文,Spring Boot 将运行所有 bean。CommandLineRunner
  • 此运行器将请求您刚刚创建的副本。EmployeeRepository
  • 使用它,它将创建两个实体并存储它们。

右键单击并运行 ,这就是你得到的:​​PayRollApplication​

显示数据预加载的控制台输出片段

...
2018-08-09 11:36:26.169 INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=1, name=Bilbo Baggins, role=burglar)
2018-08-09 11:36:26.174 INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=2, name=Frodo Baggins, role=thief)
...

这不是整个日志,而只是预加载数据的关键位。(事实上,请查看整个控制台。这是光荣的。

HTTP是平台

要用 Web 层包装存储库,您必须转向 Spring MVC。多亏了 Spring Boot,基础设施中几乎没有代码。相反,我们可以专注于行动:

nonrest/src/main/java/payroll/EmployeeController.java

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​​指示每个方法返回的数据将直接写入响应正文,而不是呈现模板。
  • 构造函数将 A 注入控制器。EmployeeRepository
  • 我们为每个操作都有路由(、 和 , 对应于 HTTP 、、 和调用)。(注意:阅读每种方法并了解它们的作用很有用。@GetMapping@PostMapping@PutMapping@DeleteMappingGETPOSTPUTDELETE
  • ​EmployeeNotFoundException​​是用于指示何时查找但未找到员工的例外。

nonrest/src/main/java/payroll/EmployeeNotFoundException.java

package payroll;

class EmployeeNotFoundException extends RuntimeException

EmployeeNotFoundException(Long id)
super("Could not find employee " + id);

当抛出 an 时,Spring MVC 配置的这个额外花絮用于渲染 HTTP 404:​​EmployeeNotFoundException​

nonrest/src/main/java/payroll/EmployeeNotFoundAdvice.java

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​​将建议配置为仅在引发 an 时响应。EmployeeNotFoundException
  • ​@ResponseStatus​​说发出一个,即一个HTTP 404HttpStatus.NOT_FOUND
  • 建议的正文生成内容。在这种情况下,它会给出异常的消息。

若要启动应用程序,请右键单击 ,然后选择“从 IDE 中运行”,或者:​​public static void main​​​​PayRollApplication​

Spring Initializr 使用 maven 包装器,所以键入以下内容:

$ ./mvnw clean spring-boot:run

或者使用您安装的 maven 版本键入以下内容:

$ mvn clean spring-boot:run

当应用程序启动时,我们可以立即询问它。

$ curl -v localhost:8080/employees

这将产生:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Thu, 09 Aug 2018 17:58:00 GMT
<
* Connection #0 to host localhost left intact
["id":1,"name":"Bilbo Baggins","role":"burglar","id":2,"name":"Frodo Baggins","role":"thief"]

在这里,您可以看到压缩格式的预加载数据。

如果您尝试查询不存在的用户...

$ curl -v localhost:8080/employees/99

你得到...

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees/99 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 26
< Date: Thu, 09 Aug 2018 18:00:56 GMT
<
* Connection #0 to host localhost left intact
Could not find employee 99

此消息很好地显示了带有自定义消息“找不到员工 99”的 HTTP 404 错误。

不难显示当前编码的交互...


如果您使用 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 localhost:8080/employees/3 -H Content-type:application/json -d "name": "Samwise Gamgee", "role": "ring bearer"

我们可以看到输出中反映的变化。

"id":3,"name":"Samwise Gamgee","role":"ring bearer"

构建服务的方式可能会产生重大影响。在这种情况下,我们说更新,但替换是一个更好的描述。例如,如果未提供名称,则会将其注销。

最后,您可以像这样删除用户:

$ curl -X DELETE localhost:8080/employees/3

# Now if we look again, its gone
$ curl localhost:8080/employees/3
Could not find employee 3

这一切都很好,但是我们有RESTful服务吗?(如果你没有抓住提示,答案是否定的。

缺少什么?

是什么让事物变得令人不安?

到目前为止,您有一个基于 Web 的服务来处理涉及员工数据的核心操作。但这还不足以让事情变得“RESTful”。

  • 漂亮的网址,比如不是REST。/employees/3
  • 仅仅使用 、 等不是 REST。GETPOST
  • 布置所有 CRUD 操作不是 REST。

事实上,到目前为止我们构建的内容更好地描述为 RPC远程过程调用)。这是因为没有办法知道如何与此服务交互。如果您今天发布了此内容,您还必须编写文档或在某个包含所有详细信息的开发人员门户。

Roy Fielding的这句话可能进一步为RESTRPC之间的区别提供了线索:


我对将任何基于 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 项目。要将服务升级到 RESTful,请将以下内容添加到您的构建中:

将春季 HATEOAS 添加到​​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,并使用静态辅助程序方法来构建这些链接。如果您在项目中使用 Spring WebFlux,则必须改用 .​​WebMvcLinkBuilder​​​​WebFluxLinkBuilder​


这与我们之前的情况非常相似,但有一些事情发生了变化:

  • 该方法的返回类型已从 更改为 。 是Spring HATEOAS的一个通用容器,它不仅包含数据,还包含链接集合。EmployeeEntityModel<Employee>EntityModel<T>
  • ​linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()​​要求Spring HATEOAS构建指向 的方法的链接,并将其标记为EmployeeControllerone()自我链接。
  • ​linkTo(methodOn(EmployeeController.class).all()).withRel("employees")​​要求Spring HATEOAS建立与聚合根的链接,并将其称为“员工”。all()

我们所说的“建立链接”是什么意思?Spring HATEOAS 的核心类型之一是 。它包括一个 URI 和一个 rel(关系)。链接是赋予网络权力的东西。在万维网出现之前,其他文档系统会呈现信息或链接,但正是文档与这种关系元数据的链接将网络拼接在一起。​​Link​

Roy Fielding 鼓励使用与 Web 成功的相同技术构建 API,链接就是其中之一。

如果重新启动应用程序并查询 Bilbo 的员工记录,则得到的响应将与之前略有不同:

冰壶更漂亮


当您的 curl 输出变得更加复杂时,它可能会变得难以阅读。使用这个或其他提示要美化 curl 返回的 JSON,请执行以下操作:




# The indicated part pipes the output to json_pp and asks it to make your JSON pretty. (Or use whatever tool you like!)
# v------------------v
curl -v localhost:8080/employees/1 | json_pp



REST单个员工的表示形式


"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links":
"self":
"href": "http://localhost:8080/employees/1"
,
"employees":
"href": "http://localhost:8080/employees"


此解压缩输出不仅显示您之前看到的数据元素(和 ),还显示包含两个 URI 的条目。整个文档的格式为​​id​​​​name​​​​role​​​​_links​​哈尔.

HAL 是一种轻量级媒体类型这不仅允许对数据进行编码,还允许对超媒体控件进行编码,提醒消费者他们可以导航到 API 的其他部分。在这种情况下,有一个“self”链接(有点像代码中的语句)以及一个返回​​this​​聚合根目录​.

为了使聚合根也更 RESTful,您需要包含顶级链接,同时还要包含任何 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<>​​是另一个春天的HATEOAS容器;它旨在封装资源集合,而不是像以前那样封装单个资源实体。,也允许您包含链接。​​EntityModel<>​​​​CollectionModel<>​

不要让第一句话溜走。“封装集合”是什么意思?员工集合?

差一点。

由于我们谈论的是 REST,它应该封装员工资源的集合。

这就是为什么你获取所有员工,然后将它们转换为对象列表。(感谢Java 8 Streams!​​EntityModel<Employee>​

如果重新启动应用程序并获取聚合根,则可以看到它现在的外观。

REST 员工资源集合的表示形式


"_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 服务随着时间的推移而发展成为可能。可以保留现有链接,同时将来可以添加新链接。新客户端可以利用新链接,而旧客户端可以在旧链接上维持自身。如果服务被重新定位和移动,这将特别有用。只要保持链接结构,客户端仍然可以找到事物并与之交互。

简化链接创建

在前面的代码中,您是否注意到单个员工链接创建的重复性?用于向员工提供单个链接以及创建指向聚合根的“员工”链接的代码显示两次。如果这引起了您的关注,那很好!有一个解决方案。

简而言之,您需要定义一个将对象转换为对象的函数。虽然您可以轻松地自己编写此方法,但实现Spring HATEOAS的接口是有好处的 - 它将为您完成工作。​​Employee​​​​EntityModel<Employee>​​​​RepresentationModelAssembler​

evolution/src/main/java/payroll/EmployeeModelAssembler.java

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 所有模型的抽象基类是 。但为了简单起见,我建议使用作为您的机制,轻松地将所有 POJO 包装为模型。​​RepresentationModel​​​​EntityModel<T>​

要利用此汇编程序,您只需通过在构造函数中注入汇编器来更改 。​​EmployeeController​

将员工模型汇编器注入控制器

@RestController
class EmployeeController

private final EmployeeRepository repository;

private final EmployeeModelAssembler assembler;

EmployeeController(EmployeeRepository repository, EmployeeModelAssembler assembler)

this.repository = repository;
this.assembler = assembler;


...

从这里,您可以在单项员工方法中使用该汇编程序:

使用汇编程序获取单个项目资源

@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());

同样,代码几乎相同,但是您可以将所有创建逻辑替换为 .借助 Java 8 方法引用,插入它并简化控制器非常容易。​​EntityModel<Employee>​​​​map(assembler::toModel)​

Spring HATEOAS的一个关键设计目标是使做正确的事情™变得更容易。在此方案中:将超媒体添加到服务中,而无需对内容进行硬编码。

在这个阶段,你已经创建了一个Spring MVC REST控制器,它实际上产生了超媒体驱动的内容!不使用 HAL 的客户端可以在使用纯数据时忽略额外的位。使用 HAL 的客户端可以导航您的授权 API。

但这并不是使用 Spring 构建真正的 RESTful 服务所需的唯一内容。

不断发展的 REST API

通过一个额外的库和几行额外的代码,您已经将超媒体添加到了应用程序中。但这并不是使您的服务成为 RESTful 所需的唯一内容。REST的一个重要方面是它既不是技术堆栈也不是单一标准。

REST 是体系结构约束的集合,采用这些约束后,应用程序将更具弹性。弹性的一个关键因素是,当您升级服务时,您的客户不会遭受停机的影响。

在“过去”的日子里,升级因破坏客户而臭名昭著。换句话说,升级到服务器需要更新客户端。在当今时代,升级花费数小时甚至数分钟的停机时间可能会造成数百万美元的收入损失。

一些公司要求您向管理层提供计划,以尽量减少停机时间。过去,您可以在负载最小的星期日凌晨 2:00 进行升级。但在当今与其他时区的国际客户的基于互联网的电子商务中,这种策略并不那么有效。

基于 SOAP 的服务和基于 CORBA 的服务非常脆弱。很难推出同时支持新旧客户端的服务器。使用基于 REST 的实践,这要容易得多。特别是使用弹簧堆栈。

支持对 API 的更改

想象一下这个设计问题:你已经推出了一个具有基于此记录的系统。该系统是一个重大打击。您已经将系统出售给无数企业。突然间,需要将员工的名字拆分并出现。​​Employee​​​​firstName​​​​lastName​

呃哦。没想到。

在打开类并将单个字段替换为 和 之前,请停下来思考一下。这会破坏任何客户吗?升级它们需要多长时间。您甚至控制访问您的服务的所有客户端吗?​​Employee​​​​name​​​​firstName​​​​lastName​

停机时间 = 亏损。管理层准备好了吗?

有一个古老的策略比 REST 早几年。

切勿删除数据库中的列。

— 未知

您始终可以向数据库表添加列(字段)。但不要拿走一个。RESTful 服务中的原理是相同的。

向 JSON 表示形式添加新字段,但不要删除任何字段。喜欢这个:

支持多个客户端的 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​

  • 字段已替换为 和 。namefirstNamelastName
  • 定义了旧属性的“虚拟”获取器。它使用 and 字段来生成值。namegetName()firstNamelastName
  • 还定义了旧属性的“虚拟”资源库。它解析传入的字符串并将其存储到适当的字段中。namesetName()

当然,并非对 API 的每次更改都像拆分字符串或合并两个字符串那样简单。但对于大多数场景,想出一组转换肯定不是不可能,对吧?


不要忘记更改预加载数据库的方式(在 中)以使用此新构造函数。​​LoadDatabase​




log.info("Preloading " + repository.save(new Employee("Bilbo", "Baggins", "burglar")));
log.info("Preloading " + repository.save(new Employee("Frodo", "Baggins", "thief")));



正确的回应

朝着正确方向迈出的另一步是确保每个 REST 方法返回正确的响应。更新 POST 方法,如下所示:

处理“旧”和“新”客户端请求的 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);
  • 新对象将像以前一样保存。但是生成的对象是使用 .EmployeeEmployeeModelAssembler
  • Spring MVC 用于创建 HTTP 201 Created 状态消息。这种类型的响应通常包括位置响应标头,我们使用从模型的自相关链接派生的 URI。ResponseEntity
  • 此外,返回已保存对象的基于模型的版本。

进行这些调整后,您可以使用同一终端节点创建新的员工资源,并使用旧字段:​​name​

$ curl -v -X POST localhost:8080/employees -H Content-Type:application/json -d "name": "Samwise Gamgee", "role": "gardener"

输出如下所示:

> POST /employees HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 46
>
< Location: http://localhost:8080/employees/3
< Content-Type: application/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Fri, 10 Aug 2018 19:44:43 GMT
<

"id": 3,
"firstName": "Samwise",
"lastName": "Gamgee",
"role": "gardener",
"name": "Samwise Gamgee",
"_links":
"self":
"href": "http://localhost:8080/employees/3"
,
"employees":
"href": "http://localhost:8080/employees"


这不仅在 HAL 中呈现了生成的对象(以及 /),而且还用 .超媒体驱动的客户端可以选择“浏览”这个新资源并继续与之交互。​​name​​​​firstName​​​​lastName​​​​http://localhost:8080/employees/3​

PUT 控制器方法需要类似的调整:

为不同的客户端处理 PUT

构建RESTful服务(使用Spring Data JPA)

Spring boot Mybatis整合构建Rest服务(超细版)

使用 Spring 创建“Hello, World”超媒体驱动的 REST Web 服务

REST式的web服务

JAX-RS(基于Jersey) + Spring 4.x + MyBatis构建REST服务架构

如何快速构建基于Spring4.0的Rest API