Spring Boot-用于RESTful服务的HATEOAS

Spring Boot - HATEOAS for RESTful Services

本指南将帮助您使用Spring Boot为REST API /服务实现HATEOAS。

你将学习

  • 什么是HATEOAS?

  • 为什么需要HATEOAS?

  • 如何通过Spring Boot实现HATEOAS?

  • HATEOAS最佳做法是什么?

  • 10步参考课程

  • Spring初学者框架,共10个步骤

  • 面向初学者的Spring Boot的10个步骤

  • Spring MVC的10个步骤

  • JPA和Hibernate的10个步骤

  • 5个步骤的Eclipse初学者教程

  • 5个步骤的Maven初学者教程

  • 5个步骤的JUnit初学者教程

  • 5个步骤的Mockito初学者教程

  • 28分钟内完成课程指南

  • 项目代码结构

    以下屏幕快照显示了我们将创建的项目的结构。Image

    一些细节:

  • SpringBoot2RestServiceApplication.java-使用Spring Initializer生成的Spring Boot Application类。 此类充当应用程序的启动点。

  • pom.xml-包含构建此项目所需的所有依赖项。 我们将使用Spring Boot Starter AOP。

  • Student.java-学生JPA实体

  • StudentRepository.java-学生JPA存储库。 这是使用Spring Data JpaRepository创建的。

  • StudentResource.java-Spring Rest Controller公开学生资源上的所有服务。

  • data.sql-学生表的初始数据。 从实体创建表后,Spring Boot将执行此脚本。

  • Maven 3.0+是您的构建工具

  • 您最喜欢的IDE。 我们使用Eclipse。

  • JDK 1.8以上

  • 使用代码示例完成Maven项目

    我们的Github存储库包含所有代码示例-https://github.com/in28minutes/spring-boot-examples/tree/master/spring-boot-2-rest-service-hateoas

    理查森成熟度模型

    理查森成熟度模型定义了Restful Web服务的成熟度级别。 以下是不同的级别及其特征。

  • 级别0:以REST风格公开SOAP Web服务。使用REST公开基于操作的服务(http:// server / getPosts,http:// server / deletePosts,http:// server / doThis,http:// server / doThat等)。

  • 级别1:使用适当的URI(使用名词)公开资源。例如:http:// server / accounts,http:// server / accounts / 10。但是,不使用HTTP方法。

  • 级别2:资源使用正确的URI + HTTP方法。例如,要更新帐户,您可以执行PUT。要创建帐户,请执行POST。 Uri的外观类似于posts / 1 / comments / 5和account / 1 / friends / 1。

  • 级别3:HATEOAS(作为应用程序状态引擎的超媒体)。您不仅会告诉您所请求的信息,还会告诉您服务使用者可以采取的下一步行动。当请求有关Facebook用户的信息时,REST服务可以返回用户详细信息以及有关如何获取其最近帖子,如何获取其最近评论以及如何检索其朋友列表的信息。

  • 什么是HATEOAS?

    HATEOAS代表"超媒体是应用程序状态的引擎"。 这是一个复杂的首字母缩写。 让我们为您解码。

    当您访问网页时会看到什么?

    您想要查看的数据。 这就是全部? 您还将看到链接和按钮以查看相关数据。

    例如,如果您转到学生页面,您将看到

  • 学生简介

  • 链接以编辑和删除学生详细信息

  • 链接以查看其他学生的详细信息

  • 链接以查看学生的课程和成绩的详细信息

  • HATEOAS将相同的概念引入RESTful Web服务。

    当请求资源的某些详细信息时,您将提供资源详细信息,相关资源的详细信息以及可以对资源执行的可能操作。 例如,当请求有关Facebook用户的信息时,REST服务可以返回以下内容

  • 使用者详细资料

  • 获得他最近帖子的链接

  • 获得他最近评论的链接

  • 链接以检索他朋友的列表。

  • 使用REST资源引导项目

    在该系列的上一篇文章中,我们使用资源公开了CRUD方法来设置一个简单的RESTful服务。

    我们将使用相同的示例来讨论HATEOAS。

    使用Spring Boot实施HATEOAS

    Spring Boot为HATEOAS提供了一个入门工具。 在pom.xml中包含依赖项

    1
    2
    3
    4
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>

    Spring Boot HATEOAS Starter中的组件

    下面列出了spring-boot-starter-hateoas中的一些重要依赖项。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.0.0.M6</version>
        <scope>compile</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.hateoas</groupId>
        <artifactId>spring-hateoas</artifactId>
        <version>0.24.0.RELEASE</version>
        <scope>compile</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.plugin</groupId>
        <artifactId>spring-plugin-core</artifactId>
        <version>1.2.0.RELEASE</version>
        <scope>compile</scope>
      </dependency>

    最重要的依存关系是spring-hateoas

    增强资源以返回HATEOAS响应

    要实施HATEOAS,我们需要在响应中包含相关资源。

    代替学生,我们使用返回类型Resource<Student>。资源是包装域对象并允许添加链接的简单类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @GetMapping("/students/{id}")
    public Resource&lt;Student&gt; retrieveStudent(@PathVariable long id) {
      Optional<Student> student = studentRepository.findById(id);

      if (!student.isPresent())
        throw new StudentNotFoundException("id-" + id);

      Resource&lt;Student&gt; resource = new Resource&lt;Student&gt;(student.get());

      ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents());

      resource.add(linkTo.withRel("all-students"));

      return resource;
    }

    我们创建一个新资源。

    1
      Resource&lt;Student&gt; resource = new Resource&lt;Student&gt;(student.get());

    我们将链接添加到检索所有学生方法的链接。

    1
    2
    3
      ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents());

      resource.add(linkTo.withRel("all-students"));

    当我们执行对http://localhost:8080/students/10001的GET请求时的响应如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
     "id": 10001,
     "name":"Ranga",
     "passportNumber":"E1234567",
     "_links": {
       "all-students": {
         "href":"http://localhost:8080/students"
        }
      }
    }

    您会看到有一个新部分_links,其中有指向所有学生资源的链接。

    使用HATEOAS增强其他资源

    上面的示例涵盖了使用HATEOAS增强资源的重要概念。

    但是,您必须做出重要的决定:

  • 与特定资源相关的重要资源有哪些?

  • 继续并通过更多HATEOAS链接增强应用程序。

    下一步

  • 了解Spring Boot的基础知识-Spring Boot与Spring vs Spring MVC,自动配置,Spring Boot Starter项目,Spring Boot Starter父级,Spring Boot Initializr

  • 通过Spring Boot学习RESTful和SOAP Web服务

  • 通过Spring Boot和Spring Cloud学习微服务

  • 观看Spring Framework面试指南-200多个问题与解答

  • 完整的代码示例

    /pom.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>

      <groupId>com.in28minutes.springboot.rest.example</groupId>
      <artifactId>spring-boot-2-rest-service-hateoas</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <packaging>jar</packaging>

      <name>spring-boot-2-rest-service</name>
      <description>Spring Boot 2 and REST - Example Project</description>

      <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.M6</version>
        <relativePath/> <!-- lookup parent from repository -->
      </parent>

      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
      </properties>

      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>

        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-devtools</artifactId>
          <scope>runtime</scope>
        </dependency>
        <dependency>
          <groupId>com.h2database</groupId>
          <artifactId>h2</artifactId>
          <scope>runtime</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
        </dependency>
      </dependencies>

      <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
        </plugins>
      </build>

      <repositories>
        <repository>
          <id>spring-snapshots</id>
          <name>Spring Snapshots</name>
          <url>https://repo.spring.io/snapshot</url>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </repository>
        <repository>
          <id>spring-milestones</id>
          <name>Spring Milestones</name>
          <url>https://repo.spring.io/milestone</url>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>

      <pluginRepositories>
        <pluginRepository>
          <id>spring-snapshots</id>
          <name>Spring Snapshots</name>
          <url>https://repo.spring.io/snapshot</url>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </pluginRepository>
        <pluginRepository>
          <id>spring-milestones</id>
          <name>Spring Milestones</name>
          <url>https://repo.spring.io/milestone</url>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>


    </project>

    /src/main/java/com/in28minutes/springboot/rest/example/SpringBoot2RestServiceApplication.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.in28minutes.springboot.rest.example;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class SpringBoot2RestServiceApplication {

      public static void main(String[] args) {
        SpringApplication.run(SpringBoot2RestServiceApplication.class, args);
      }
    }

    /src/main/java/com/in28minutes/springboot/rest/example/student/Student.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package com.in28minutes.springboot.rest.example.student;

    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;

    @Entity
    public class Student {
      @Id
      @GeneratedValue
      private Long id;
      private String name;
      private String passportNumber;

      public Student() {
        super();
      }

      public Student(Long id, String name, String passportNumber) {
        super();
        this.id = id;
        this.name = name;
        this.passportNumber = passportNumber;
      }
      public Long getId() {
        return id;
      }
      public void setId(Long id) {
        this.id = id;
      }
      public String getName() {
        return name;
      }
      public void setName(String name) {
        this.name = name;
      }
      public String getPassportNumber() {
        return passportNumber;
      }
      public void setPassportNumber(String passportNumber) {
        this.passportNumber = passportNumber;
      }

    }

    /src/main/java/com/in28minutes/springboot/rest/example/student/StudentNotFoundException.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.in28minutes.springboot.rest.example.student;

    public class StudentNotFoundException extends RuntimeException {

      public StudentNotFoundException(String exception) {
        super(exception);
      }

    }

    /src/main/java/com/in28minutes/springboot/rest/example/student/StudentRepository.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.in28minutes.springboot.rest.example.student;

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

    @Repository
    public interface StudentRepository extends JpaRepository<Student, Long>{

    }

    /src/main/java/com/in28minutes/springboot/rest/example/student/StudentResource.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    package com.in28minutes.springboot.rest.example.student;

    import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
    import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

    import java.net.URI;
    import java.util.List;
    import java.util.Optional;

    import javax.validation.Valid;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.hateoas.Resource;
    import org.springframework.hateoas.mvc.ControllerLinkBuilder;
    import org.springframework.http.ResponseEntity;
    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;
    import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

    @RestController
    public class StudentResource {

      @Autowired
      private StudentRepository studentRepository;

      @GetMapping("/students")
      public List<Student> retrieveAllStudents() {
        return studentRepository.findAll();
      }

      @GetMapping("/students/{id}")
      public Resource&lt;Student&gt; retrieveStudent(@PathVariable long id) {
        Optional<Student> student = studentRepository.findById(id);

        if (!student.isPresent())
          throw new StudentNotFoundException("id-" + id);

        Resource&lt;Student&gt; resource = new Resource&lt;Student&gt;(student.get());

        ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents());

        resource.add(linkTo.withRel("all-students"));

        return resource;
      }

      @DeleteMapping("/students/{id}")
      public void deleteStudent(@PathVariable long id) {
        studentRepository.deleteById(id);
      }

      @PostMapping("/students")
      public ResponseEntity<Object> createStudent(@RequestBody Student student) {
        Student savedStudent = studentRepository.save(student);

        URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
            .buildAndExpand(savedStudent.getId()).toUri();

        return ResponseEntity.created(location).build();

      }

      @PutMapping("/students/{id}")
      public ResponseEntity<Object> updateStudent(@RequestBody Student student, @PathVariable long id) {

        Optional<Student> studentOptional = studentRepository.findById(id);

        if (!studentOptional.isPresent())
          return ResponseEntity.notFound().build();

        student.setId(id);

        studentRepository.save(student);

        return ResponseEntity.noContent().build();
      }
    }

    /src/main/resources/data.sql

    1
    2
    3
    4
    5
    insert into student
    values(10001,'Ranga', 'E1234567');

    insert into student
    values(10002,'Ravi', 'A1234568');

    /src/test/java/com/in28minutes/springboot/rest/example/SpringBoot2RestServiceApplicationTests.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package com.in28minutes.springboot.rest.example;

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringBoot2RestServiceApplicationTests {

      @Test
      public void contextLoads() {
      }

    }