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分钟内完成课程指南
项目代码结构
以下屏幕快照显示了我们将创建的项目的结构。
一些细节:
SpringBoot2RestServiceApplication.java-使用Spring Initializer生成的Spring Boot Application类。 此类充当应用程序的启动点。
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中的组件
下面列出了
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> |
最重要的依存关系是
增强资源以返回HATEOAS响应
要实施HATEOAS,我们需要在响应中包含相关资源。
代替学生,我们使用返回类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @GetMapping("/students/{id}") public Resource<Student> retrieveStudent(@PathVariable long id) { Optional<Student> student = studentRepository.findById(id); if (!student.isPresent()) throw new StudentNotFoundException("id-" + id); Resource<Student> resource = new Resource<Student>(student.get()); ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents()); resource.add(linkTo.withRel("all-students")); return resource; } |
我们创建一个新资源。
1 | Resource<Student> resource = new Resource<Student>(student.get()); |
我们将链接添加到检索所有学生方法的链接。
1 2 3 | ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents()); resource.add(linkTo.withRel("all-students")); |
当我们执行对
1 2 3 4 5 6 7 8 9 10 | { "id": 10001, "name":"Ranga", "passportNumber":"E1234567", "_links": { "all-students": { "href":"http://localhost:8080/students" } } } |
您会看到有一个新部分
使用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<Student> retrieveStudent(@PathVariable long id) { Optional<Student> student = studentRepository.findById(id); if (!student.isPresent()) throw new StudentNotFoundException("id-" + id); Resource<Student> resource = new Resource<Student>(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() { } } |