资讯详情

使用 Spring 构建 REST 服务

注:本文是 spring 官网 guide Tutorials 第一篇文章的一部分(Building REST services with Spring)中文翻译版。因为译者不是专业翻译,所以一定有一些不流畅的地方,会多次修改迭代。如果发现问题,请纠正。文章翻译仅供参考!

英文链接:https://spring.io/guides/tutorials/rest/

项目的 github 地址:https://github.com/spring-guides/tut-rest 在译者的个人博客上阅读本文。

REST 由于易于构建,易于使用的特点已迅速成为网络上构建网络服务的实际标准。

关于 REST 有太多的讨论如何适合微服务。在这个教程中,我们只建立它 RESTful 服务。

为什么是REST?REST接受网络的戒律,包括它的结构、好处和其他一切。这并不奇怪,因为它的作者Roy Fielding它参与了大约十个管理网络运行模式的规范。

为什么选择 REST?因为 REST 包含了 web 规则包括它的结构、优势和其他一切。这并不奇怪,因为它的作者 Roy Fielding 管理网络运行管理网络运行方式的规范。

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

  • 适当的操作(GET, POST, PUT, DELETE,…)

  • 缓存

  • 重定向和转发

  • 安全(加密和身份验证)

这些都是建立灵活服务的关键因素。但并非全部。该网络由许多小规格组成,因此它很容易发展,而不会陷入标准战争的泥潭。

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

通过建立在HTTP之上,REST APIs提供以下施工方法:

  • 向后兼容的API

  • 可演化的API (原文为:Evolvable APIs 作者认为可以理解为可迭代、扩展和更新API)

  • 可扩展服务

  • 有保障的服务

  • 从无状态到有状态的服务范围

重要的是要认识到,REST无论多么普遍,它本身都不是一个标准,而是一种方法,一种风格,一套对你的架构的约束,可以帮助你建立一个网络规模的系统。我们将在本教程中使用它 Spring 组合构建一个 RESTful 同时使用服务 REST 无堆栈的特点。

入门

当我们通过本教程构建项目时,我们将使用它 Spring Boot 。进入 Spring Initializr ,在项目中添加以下依赖项:

  • Web
  • JPA
  • H2

将名称改为 “Payroll然后选择 “Generate Project载一个.zip文件。解压它。你会在里面找到一个简单的,基于它Maven包括一个项目pom.xml建筑文件(注:可使用Gradle。本教程的例子都是基于Maven的)。

Spring Boot可以在任何IDE中工作。你可以使用Eclipse、IntelliJ IDEA、Netbeans等。Spring Tool Suite是于开源Eclipse的IDE它提供了分布Eclipse的Java EE超集的分布。它包括功能使用Spring应用程序更容易。它绝不必要。但是如果你想让你的按钮有额外的动力,考虑使用它。

译者使用了 idea ,而 spingguide 中推荐了 STS ,其实没有影响,请选择自己喜欢的。 IDE 可以搭建。

到目前为止…

让我们从最简单的事情开始。事实上,为了尽可能简单,我们甚至可以忽略它 REST 概念。(稍后,我们将加入 REST 了解区别)。

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

接下来,在我们的系统中定义了一个 Employee 类。

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 在数据存储中。

  • idrolename 是 Employee 域对象的属性。id 用更多的 JPA 注释标记表明它是主键并通过 JPA 自动填充供应商。

当我们需要创建一个新的例子时,还没有 id 自定义构造函数将创建。

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

Spring Data JPA 存储库是支持后端数据存储创建、读取、更新和删除记录方法的接口。一些存储库还支持数据分页,并在适当的时候进行排序。Spring Data 根据接口中方法命名的约定来合成实现。

后半句的原文是:Spring Data synthesizes implementations based on conventions found in the naming of the methods in the interface. convention有传统惯例的意思,这里翻译为约定…

除了 JPA ,还有各种各样的存储库。您可以使用它Spring Data MongoDB、Spring Data GemFire、Spring Data Cassandra等等。我们将坚持使用本教程JPA。

Spring 使访问数据更容易。简单声明以下内容 EmployeeRepository 接口,我们将能够自动:

  • 创建新员工数据

  • p>更新现有的数据

  • 删除员工数据

  • 查找雇员(一个、全部或通过简单或复杂属性进行搜索)的数据

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 的 JpaRepository 的接口,指定域类型为Employee,id 类型为 Long 。

Spring Data 的存储库解决方案可以使我们避开数据存储细节,转而使用特定于领域的术语解决大部分问题。

信不信由你,这就足以启动一个应用程序了! 一个 Spring Boot 应用程序至少要有一个公共静态 void main 入口点和 @SpringBootApplication 注解。这告诉 Spring Boot 尽可能地向我们提供帮助。

原话为:This tells Spring Boot to help out, wherever possible. 笔者理解为:使用注解以及 void main 入口 将使得 Spring Boot 向我们提供尽可能多的功能以帮助我们实现程序。

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 将运行所有的 CommandLineRunner Bean

  • 这个运行器将请求一个你刚刚创建的 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)
...

这并不是整个日志,只是预加载数据的关键部分。 (实际上,请查看整个控制台,这是值得称赞的。)

原句:This isn’t the log, but just the key bits of preloading data. (Indeed, check out the whole console. It’s glorious.)

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 表示每个方法返回的数据将直接写入响应体(response body)中,而不是渲染成一个模板。
  • EmployeeRepository 通过构造函数被注入到控制器(Controller)中。
  • 我们为每个操作(@GetMapping@PostMapping@PutMapping@DeleteMapping,对应于 HTTP GET、POST、PUT 和 DELETE 调用)设置了路由。 (NOTE:阅读每种方法并了解它们的作用对你是有很帮助的。)
  • EmployeeNotFoundException 是一个异常,用于表示当查询一个雇员但没有找到时。

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

package payroll;

class EmployeeNotFoundException extends RuntimeException {

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

当抛出 EmployeeNotFoundException 时,使用这个额外的 Spring MVC 配置来呈现 HTTP 404:

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表示该 advice 将被直接呈现到响应体(ResponseBody)中。
  • @ExceptionHandler 将 advice 配置为只在抛出 EmployeeNotFoundException 时作出响应。
  • @ResponseStatus 表示发出 HttpStatus.NOT_FOUND ,即一个HTTP 404。
  • advice 的主要内容。在这种情况下,它会给出异常消息。

原文:The body of the advice generates the content. In this case, it gives the message of the exception.

要启动应用程序,可以在 PayRollApplication 中右键单击 public static void main ,并选择从 IDE 运行( from your IDE),或者:

若 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

这条信息很好地显示了一个 HTTP 404 错误与自定义信息 Could not find employee 99 。

显示当前代码的交互并不难。

如果您使用Windows命令提示符发出cURL命令,下面的命令可能无法正常工作。您必须选择支持单引号参数的终端,或者使用双引号,然后转义JSON中的参数。

笔者note:只需将 :

$ curl -X POST localhost:8080/employees -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'

改为这样即可:

$ curl -X POST localhost:8080/employees -H "Content-type:application/json" -d ""{ 
         \"name\": \"Samwise Gamgee\", \"role\": \"gardener\"}"

为了创建一条新的雇员记录,我们在终端中使用以下命令–开头的$表示后面的内容是终端命令。

(以下遵循原 guide 上代码,如果是 windows 系统 cmd 或 Conemu,请参考笔者上面的转义方式)

$ curl -X POST localhost:8080/employees -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'

然后,它存储新创建的雇员并将其送回给我们。

{ 
        "id":3,"name":"Samwise Gamgee","role":"gardener"}

你可以更新用户。让我们改变他的角色(role)。

$ 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"}

您构建服务的方式可能会对系统产生重大影响。在这种情景下,我们说的是更新(update),但其实替换(replace)是更贴切的描述。例如,如果服务本身并没有提供名称,我们这样做将会返回空值。

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

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

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

这很好,但是我们有 RESTful 服务吗?(如果你没有领会其中的暗示,答案是没有。)

缺少了什么?

什么要素可以使服务变得 RESTful?

到目前为止,你已经拥有了一个基于 web 的服务,该服务处理涉及员工数据的核心操作。但这还不足以让一切变得“RESTful”。

  • /employees/3 这样漂亮的 url 不是 REST 。

  • 仅仅使用 GET、POST 等方法,这不是REST。

  • 把所有的 CRUD 操作摆出来也不是 REST。

事实上,到目前为止,我们所建立的东西被称做 RPC(远程程序调用)更好。这是因为没有办法知道如何与这个服务互动。如果你今天发布这个服务,你还得写一份文档,或者在某个地方托管一个开发人员的门户网站,其中包含项目的所有细节。

Roy Fielding的这句话可能会进一步揭示REST和RPC之间的区别:

我是越来越失望了,许多人把任何基于 HTTP 的接口称作 REST API ,眼前的例子就是 SocialSite REST API 。那是RPC,实实在在的RPC。它与显示如此耦合,再差也莫过于此。

基于超文本概念,如何才能确保清晰的REST架构风格呢?这样来说吧,如果应用程序状态引擎(即API)不是由超文本驱动的,那就不是RESTful也不是REST的API。就这么简单。某些REST方面的破手册是否该修正一下呢?

翻译参考:https://blog.csdn.net/weixin_43318367/article/details/108745724

原文:

I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.

What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?

— Roy Fielding https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

在我们的表示中不包含超媒体的副作用是客户端必须硬编码 uri 来导航 API 。这导致了我们的服务与网络电子商务兴起之前的网站具有相同的脆弱性。这表明我们的JSON输出需要一些修改。

原文:The side effect of NOT including hypermedia in our representations is that clients MUST hard code URIs to navigate the API. This leads to the same brittle nature that predated the rise of e-commerce on the web. It’s a signal that our JSON output needs a little help.

接下来介绍一下 Spring HATEOAS ,这是一个 Spring 项目,它旨在帮助你编写超媒体驱动的输出( hypermedia-driven outputs)。为将你的服务升级为 RESTful ,请将它添加到你的构建中:

将 Spring HATEOAS 添加到 pom.xml 的依赖项部分:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

这个迷你库将为我们提供定义 RESTful 服务的构造,然后以客户端可接受的格式呈现服务。

任何基于 rest 的服务的关键组成部分都是 添加相关操作的链接。为了让你的控制器(controller)更 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,使用 WebMvcLinkBuilder 的静态辅助方法(the static helper methods)来构建这些链接。如果你在你的项目中使用 Spring WebFlux,那你必须使用 WebFluxLinkBuilder

这和我们之前的做的事情非常相似,但仍有一些细微的变化:

  • 方法的返回类型从 Employee 更改为 EntityModel<Employee>EntityModel<T>是一个来自 Spring HATEOAS 的通用容器,它不仅包含数据,还包含一组链接。
  • linkTo(methodOn(EmployeeController.class).one(id)).withselfrel()要求Spring HATEOAS构建到EmployeeController one() 方法的链接,并将其标记为 self 链接。
  • linkTo(methodOn(employeecontrolle.class).all()).withrel("employees")请求 Spring HATEOAS 构建到聚合根 连接,all() ,并将其称为 “employees”。

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

原文:What do we mean by “build a link”? One of Spring HATEOAS’s core types is Link. It includes a and a (relation). Links are what empower the web. Before the World Wide Web, other document systems would render information or links, but it was the linking of documents WITH this kind of relationship metadata that stitched the web together.

Roy Fielding 鼓励使用使网络成功的相同技术来构建 api ,链接(links)就是其中之一。

如果你现在重启应用程序并查询 Bilbo 的员工记录,你会得到一个与之前稍有不同的响应:

如何让 curl 命令看着更舒服一些

当你的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

单个员工的 RESTful 表示

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

这个接触压缩格式后的输出不仅显示了前面看到的数据元素( id、name 和 role ),还显示了一个包含两个 URI 的 links 条目。整个文档使用 HAL 格式化。

HAL 是一种轻量级的媒介类型,不仅可以对数据进行编码,还可以对超媒体控制进行编码,提醒消费者可以浏览到 API 的其他部分。在这种情况下,有一个 “self” 链接(有点像代码中的 this 语句)和一个回到聚合根(aggregate root)的链接。

为了使聚合根也更加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());
}

wow! 这个方法,以前只返回了 repository.findAll(),而现在已经成熟了! 不用担心。现在让我们来对它进行拆解。

  • CollectionModel<>是另一个 Spring HATEOAS 容器;它的目标是封装资源集合——而不是单个资源实体,如前面的EntityModel<>

  • CollectionModel<>,也允许你包括链接。

    不要漏掉了第一次的申明!“ 封装集合 ” 是什么意思?封装员工的集合么?

不完全是

因为我们讨论的是 REST ,所以它应该封装员工资源的集合。

这就是为什么你要获取所有的雇员信息,但随后将其转化为 EntityModel<Employee> 对象的列表。(感谢Java 8 Streams!)

如果你重新启动应用程序并获取聚合根,你可以看到它现在是什么样子。

雇员资源集合的 RESTful 表示如下:

{ 
        
  "_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"
    }
  }
}

这个提供员工资源集合的聚合根有一个顶级的“self”链接。该 “集合” 被列在 “_embedded” 部分下方;这就是 HAL 表示集合的方式。

集合中的每个成员都有自己的信息和相关链接。

添加所有这些链接的意义是什么?它使得 REST 服务的发展成为可能。维护现有的链接的同时也可以在将来添加新的链接。较新的客户机可以利用新的链接,而遗留客户机可以在旧链接上维持自身。如果服务被重新定位和移动,这些设定将派上大用场。只要保持链接结构,客户端仍然可以查找并与事物进行交互。

简化创建链接

在前面的代码中,你是否注意到在单个雇员链接的创建中存在重复现象?为一个雇员提供单一链接的代码,以及为聚合根创建 “employees” 链接的代码,被显示了两次。如果这引起了你的关注,很好!这有一个解决方案。

简单地说,你需要定义一个函数,将 Employee 对象转换为 EntityModel<Employee> 对象。虽然你可以很容易地自己编写这个方法,但实现 Spring HATEOAS 的 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 框架的 @Component 注解,当应用程序启动时,装配器将被自动创建。

Spring HATEOAS 的所有模型的抽象基类是 RepresentationModel 。但为简单起见,我建议使用 EntityModel 作为您的机制,以便轻松地将所有 POJO 包装为模型。

为了利用这个汇编器(assembler),你只需要在构造函数中注入汇编器(assembler),就可以改变 EmployeeController 。

将 EmployeeModelAssembler 注入到控制器中

@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> 实例,而是将其委托给了汇编器。也许这看起来并不起眼。

在聚合根(the aggregate root)控制器方法中使用同样的操作,加深我们对该汇编器的认识。

使用汇编程序获取聚合根(the aggregate root)资源:

@GetMapping("/employees")
CollectionModel<EntityModel<Employee>> all() { 
        

  List<EntityModel<Employee>> employees = repository.findAll().stream() //
      
        标签: jy131变送器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台