Spring Boot「01」构建 REST API

135 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第01天,点击查看活动详情

01-如何理解 REST / RESTful

REST(REpresentational State Transfer,表征状态转移)是一种架构风格,由 Roy Fielding 首次在其博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出。遵循 REST 风格的 Web API 也称为是 REST API。

Web API (or Web Service) conforming to the REST architectural style is a REST API. [4]

与其他的架构风格一样,REST 有其特定的指导原则与实现约束。如果要称一个服务或者 API 接口是 RESTful,则该服务或接口需要满足 REST 的指导原则与实现约束。更多关于 REST 指导原则的信息,感兴趣的读者可以参考[4]。

01.1-表征性

在理解 REST 中的表征性(REpresentational )之前,首先需要明确的是资源(Resource)这一概念。假设你正在观看一部影视作品,那么它就可以称为是一种资源。如果你在开发一个公司内部的人力资源系统,每个雇员也可看作是人力资源系统中的一种资源。

表征(REpresentation)指的是信息与用户交互时的表示形式[2]。例如,我们可以向系统请求 Json 格式的电影信息,也可以请求 HTML 形式的信息,甚至是直接请求媒体文件。

01.2-状态

REST 中的状态(State)是指在特定语境中才能产生的上下文信息。例如,当观看连续剧时,当前剧集的上一集、下一集信息。这些都是一些相对的信息,只有在特定的语境下才有意义。

所以,当我们向某个 RESTful 服务请求某个资源时,我们除了获得该资源的特定表征信息外,我们还应当能够从响应中获得一些上下文信息。

01.3-转移

REST 中的转移(Transfer)是指服务器通过某种方式完成在不同状态之间的转变。例如,通过响应中的下一集信息向服务器获取下一集剧集资源。

02-Spring Boot 中构建 RESTful 服务

依赖: org.springframework.boot:spring-boot-starter-web org.springframework.boot:spring-boot-starter-data-jpa com.h2database:h2

首先,需要一个资源类实体Employee:

@Entity
class Employee {
    private @Id @GeneratedValue Long id;
    private String name;
    private String role;
		// 省略了 getters / setters 
}

其次,围绕该资源定义一部分 API(完整版的代码可以从 gitee 下载):

@RestController
class EmployeeController {
    @GetMapping("/employees")
    List<Employee> all() { ... }
    @PostMapping("/employees")
    Employee newEmployee(@RequestBody Employee newEmployee) { ... }
    @GetMapping("/employees/{id}")
    Employee one(@PathVariable Long id) { ... }
    @PutMapping("/employees/{id}")
    Employee replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) { ... }
    @DeleteMapping("/employees/{id}")
    void deleteEmployee(@PathVariable Long id) { ... }
}

然后,通过 Spring-Data-JPA,定义并生成一个持久化层(非常简单):

public interface EmployeeRepository extends JpaRepository<Employee, Long> {}

最后,将所有的部分拼装在一起:

@SpringBootApplication
public class PayrollApplication {
  private final static Logger LOGGER = LoggerFactory.getLogger(PayrollApplication.class);
	public static void main(String[] args) {
		SpringApplication.run(PayrollApplication.class, args);
	}
	@Bean
	CommandLineRunner initDatabase(EmployeeRepository repository) {
      return args -> {
          LOGGER.info("Preloading {}", repository.save(new Employee("Bilbo Baggins", "burglar")));
          LOGGER.info("Preloading {}", repository.save(new Employee("Frodo Baggins", "thief")));
      };
  }
}

我们来运行一下,mvn spring-boot:run。访问浏览器http://localhost:8080/employees

但这样一个接口能称为是 RESTful 接口吗?根据前一章节的分析,显然是不能的。

02.1-如何使接口更具 RESTful 风格

依赖: org.springframework.boot:spring-boot-starter-hateoas

我们借助 hateoas 为接口的返回值添加上下文信息,来使接口返回值具有状态。在前面定义 API 接口时,我们定义了一个all来返回所有的雇员信息,接下来我们又定义了一个可以返回某个特定雇员信息(by id)的接口one。很容易能够看出,某个特定雇员的信息是所有雇员信息的一部分。

我们对one方法进行如下改造:

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

我们首先将one的返回值从Employee修改为了EntityModel<Employee>EntityModel<T>是 spring-hateoas 中定义的 entity 的包装类,它包含了原始的 entity,以及它的一些相关链接。

然后,我们通过EntityModel.of方法将employee与两个链接关联在一起:

  1. 指向其自身的链接
  2. 指向其所属的全部雇员信息的链接

可以看出来,这些链接表述的其实是雇员信息(雇员资源的一种表征)的上下文关系,即状态。当我们再次运行程序,并访问http://localhost:8080/employees/1时,返回值就变成了如下的模样:

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

其中:_links就是 spring-hateoas 为其增加的上下文信息,搭救对应了上面提到的两个链接。

同样地,我们对all方法进行改造:

@GetMapping("/employees")
CollectionModel<EntityModel<Employee>> all() {
    final 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());

}

refs