SpringBoot构建REST服务


现在,REST已成为事实上的标准,要构建web服务,使用REST很便于生产和消费,微服务架构也有大量的文献描述如何使用REST,下面就来看看如何创建REST服务。

为什么REST?

表征状态转移(Representional State Transfer),是 Roy Fielding( HTTP 规范的主要编写者之一)博士在 2000 年他的博士论文中提出来的一种软件架构风格。它并不是一个标准,而是通过表征(Representional )来描述传输状态的一种原则。其宗旨是从资源的角度来观察整个网络,分布在各处的资源由 URI 确定,而客户端的应用通过 URI 来获取资源的表征。获得这些表征致使这些应用程序转变了其状态。随着不断获取资源的表征,客户端应用不断地在转变着其状态。

让我们来思考一下:
Marcus 是一个农民,他有 4 头猪,12 只鸡和 3 头奶牛。他现在模拟一个 REST API,而我是客户端。如果我想用 REST 来请求当前的农场状态,我仅会问:“State?”Marcus 会回答:“4 头猪、12 只鸡、3 头奶牛”。
这是 REST 最简单的一个例子。Marcus 使用表征来传输农场状态。表征的句子很简单:“4 头猪、12 只鸡、3 头奶牛”。
再往下看,看我如何让 Marcus 用 REST 方式添加 2 头奶牛?
按照常理,可以会这样说:Marcus,请在农场你再添加 2 头奶牛。难道这就是 REST 方式吗?难道就是通过这样的表征来传输状态的吗?不是的!这是一个远程过程调用,过程是给农场添加 2 头奶牛。
Marcus 很愤怒地响应到:“400,Bad Request”,你到底是什么意思?
所以,让我们重新来一次。我们怎样做到 REST 方式呢?该怎样重新表征呢?它应该是 4 头猪、12 只鸡、3 头奶牛。好,让我们再次重新表征……
我:“Marcus,……4 头猪、12 只鸡、 5 头奶牛!”
Marcus:“好的”。
我:“Marcus,现在是什么状态?”
Marcus:“4 头猪、12 只鸡、5 头奶牛”。
我:“好!”
看到了吗?就这样简单。

REST拥抱Web规范和准则,包括其构架、优点和其他。这并不奇怪,因为Roy Fielding参与了十几个治理Web如何运作的规范。
有什么好处呢?Web和其核心协议,HTTP,提供一系列特征栈:

  • 适当的操作(GET、POST、PUT、DELETE…)
  • 缓存
  • 重定向和转发
  • 安全性(加密和身份验证)

这些都是建立弹性服务的关键,但不是全部。Web是基于大量微小的规范构建的,因此,能够轻松进化而不会陷入“标准之争”。
开发人员能够利用第三方工具包来实现这些不同的规范,并立即将客户端和服务器端技术放在触手可及的地方。
通过在 HTTP 上构建,REST API 提供了如下构建的手段:

  • 向后兼容的 API
  • 可进化的 API
  • 可扩展的服务
  • 可部署的服务
  • 一系列无状态到有状态服务

重要的是要认识到,REST,无论多么无处不在,本身都不是标准,而是一种方法、一种风格、一套对架构的限制,可以帮助您构建Web扩展系统。
这里,将使用 SpringBoot 构建一个 RESTful 服务,同时利用 REST 的无堆叠功能(stackless features)。

开始

打开IDEA,创建新项目,如下图所示:
新建项目输入项目参数,点击Next,选择SpringBoot依赖,如下图所示:
项目依赖进入项目目录,运行 git init 命令,初始化 git 版本库,便于代码管理,如下图所示:
git init

管理雇员信息

从最简单的领域类创建开始,现在要构造一个简单的工资服务,需要相应的雇员信息,将相应的对象存储在 H2 数据库中并提供 JPA 进行访问。
首先,创建雇员类,代码如下:

package cn.lut.curiezhang.payroll;

import java.util.Objects;

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

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: Employee</p>
 * <p>Description: </p>
 * 简单的雇员信息
 *
 * @author Curie Zhang
 * @version Employee.java v1.0 2021/7/10 18:31 curiezhang
 */
@Entity
public class Employee {
    private @Id @GeneratedValue Long id;
    private String name;
    private String role;

    public Employee() {
    }

    public 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 + '\'' + '}';
    }
}

SpringBoot JPA 存储库支持创建、读取、更新和删除等后端数据存储记录的方法接口。某些存储库还支持数据分页和排序(在某些情况下)。Spring Data 根据接口中方法命名约定来发现对应的实现。
SpringBoot使得数据访问更加容易,只需要声明接口就能够对雇员信息进行访问,代码如下:

package cn.lut.curiezhang.payroll;

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

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: EmployeeRepository</p>
 * <p>Description: </p>
 *
 * @author Curie Zhang
 * @version EmployeeRepository.java v1.0 2021/7/10 18:49 curiezhang
 */
interface EmployeeRepository extends JpaRepository<Employee, Long> {

}

使用上述接口就可以对雇员信息进行增删改查等数据库操作。
现在就可以启动项目了,这里包含了系统自动生成的项目启动类,代码如下:

package cn.lut.curiezhang.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);
    }

}

当然,现在的项目启动,不包含数据,我们可以建立一个初始测试数据类,如下所示:

package cn.lut.curiezhang.payroll;

import org.springframework.context.annotation.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: LoadDatabase</p>
 * <p>Description: </p>
 * 加载测试数据
 *
 * @author Curie Zhang
 * @version LoadDatabase.java v1.0 2021/7/11 11:06 curiezhang
 */
@Configuration
public class LoadDatabase {
    private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);

    @Bean
    CommandLineRunner initDatabase(EmployeeRepository repository) {

        return args -> {
            log.info("加载 " + repository.save(new Employee("张三",
                    "经理")));
            log.info("加载 " + repository.save(new Employee("李四",
                    "主任")));
        };
    }
}

现在,重修启动程序,首先加载应用上下文后,将运行所有 CommandLineRunner 类,这就会调用 EmployeeRepository 类,使用它来创建 2 个实体并存储起来,从控制台可以看到输出日志,通过 H2 控制台就可以看到数据库中的 EMPLOYEE 表的结果,结果如下图所示:
数据库操作结果
因此,使用 SpringBoot 很便于进行数据库访问操作。

HTTP平台

要将存储库与 Web 层进行封装,必须使用 Spring MVC。由于 SpringBoot,几乎不需要进行底层编码。这样,就可以专注于具体的动作。现在,创建雇员的 web 端控制器,如下所示:

package cn.lut.curiezhang.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;

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: EmployeeController</p>
 * <p>Description: </p>
 * 雇员的 web 端控制器
 *
 * @author Curie Zhang
 * @version EmployeeController.java v1.0 2021/7/11 11:50 curiezhang
 */
@RestController
public class EmployeeController {
    private final EmployeeRepository repository;

    EmployeeController(EmployeeRepository repository) {
        this.repository = repository;
    }


    @GetMapping("/employees")
    List<Employee> all() {
        return repository.findAll();
    }

    @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.setEmployId(id);
                    return repository.save(newEmployee);
                });
    }

    @DeleteMapping("/employees/{id}")
    void deleteEmployee(@PathVariable Long id) {
        repository.deleteById(id);
    }
}

这里有一个 EmployeeNotFoundException 异常,代码如下:

package cn.lut.curiezhang.payroll;

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: EmployeeNotFoundException</p>
 * <p>Description: </p>
 * 没有找到雇员异常
 *
 * @author Curie Zhang
 * @version EmployeeNotFoundException.java v1.0 2021/7/11 11:57 curiezhang
 */
public class EmployeeNotFoundException extends RuntimeException {
    EmployeeNotFoundException(Long id) {
        super("没有找到雇员:" + id + "!");
    }
}

当抛出此异常时,通过 Spring MVC 配置可以显示 HTTP 404。
其实,可以使用控制器建议来提供处理,代码如下:

package cn.lut.curiezhang.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;

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: EmployeeNotFoundAdvice</p>
 * <p>Description: </p>
 * 雇员没有找到异常处理建议
 *
 * @author Curie Zhang
 * @version EmployeeNotFoundAdvice.java v1.0 2021/7/11 12:08 curiezhang
 */
@ControllerAdvice
class EmployeeNotFoundAdvice {
    @ResponseBody
    @ExceptionHandler(EmployeeNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    String employeeNotFoundHandler(EmployeeNotFoundException ex) {
        return ex.getMessage();
    }
}

现在,如果没有找到雇员信息,将直接显示异常信息。
运行代码,就可以在浏览器中访问数据库中的数据了,如下图所示:
运行结果
尝试查询不存在的雇员,结果如下:
没有找到雇员
可使用 Postman 来创建新雇员,如下图所示:
post新雇员
当然,也可以使用 curl 命令,但是 Windows 下的 curl 命令可能会出错,就不建议使用了,而且,使用 Postman 非常方便,各种 HTTP 请求都很容易实现,这里不再一一介绍。

什么才是REST的?

目前为止,已经实现了 Employee 数据的基于 Web 的服务核心操作,能够进行 CRUD 操作,但这还不足以达到 REST。

  • 严谨的 URL (例如:/employees/66)不是 REST;
  • 仅仅使用 GET、POST 等不是 REST;
  • 实现所有的 CRUD 操作也不是 REST。

事实上,现在所构建的这些是 RPC(远程过程调用)的,因为,无法知道如何与这些服务进行交互。如果你要发布信息,还必须编写文档或在某个开发者门户中详细说明这些信息。
不讨论超媒体在表示层的副作用,客户必须硬编码 URI 来导航 API,这导致了 Web 的脆弱性,也是 JSON 输出需要改进的信号。
SpringBoot 引入了 HATEOAS 帮助编写超媒体驱动的输出,为此,在项目中进入如下依赖,将服务升级成 REST 的。

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

下面,为 REST 服务添加相关链接操作,使控制器更加 REST,添加链接如下:

    /**
     * linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()要求 
     * HATEOAS 建立一个链接到 EmployeeController 的 one() 方法的自链接
     * linkTo(methodOn(EmployeeController.class).all()).withRel("employees") 
     * 要求 HATEOAS 建立一个链接到集合根 all() 方法的链接叫 "employees"
     * 
     * @param id
     * @return EntityModel<Employee> 是 HATEOAS 的通用容器,不仅包括数据也链接集合
     */
    @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"));
    }

建立链接意味着什么? HATEOAS 的核心类型是链接,包括一个 URI 和一个 rel(关系),链接增强 Web 功能,WWW 之前,其他文档系统会提供信息或链接,但含有这种关系元数据的文档的链接可以和 Web 结合得更好。
重新启动应用,可以得到更有效的展示,如下图所示:
hateoas结果
整个文档可以用 HAL(Hypertxt Application Language)进行格式化。HAL 是轻量级的媒体类型,不仅允许编码数据,还提供超媒体控制,提醒消费者并可导航到 API 的其他部分,就像这里的,自链接以及链接回聚合根。
类似的,获取聚合根:

    /**
     * linkTo(methodOn(EmployeeController.class).one(employee.getEmployId())))
     * .withSelfRel()要求 HATEOAS 建立一个链接到 EmployeeController 的 one() 
     * 方法的自链接
     * linkTo(methodOn(EmployeeController.class).all()).withRel("employees")
     * 要求 HATEOAS 建立一个链接到集合根 all() 方法的链接叫 "employees"
     * 
     * @return CollectionModel<EntityModel<Employee>>, HATEOAS 
     * 容器,封装资源集合,像 EntityModel<Employee> 一样,而不是单一资源实体,
     * CollectionModel<EntityModel<Employee>>也允许包含链接地址。
     */
    @GetMapping("/employees")
    CollectionModel<EntityModel<Employee>> all() {
        List<EntityModel<Employee>> employees = repository.findAll().stream()
                .map(employee -> EntityModel.of(employee,
                        linkTo(methodOn(EmployeeController.class).
                                one(employee.getEmployId())).withSelfRel(),
                        linkTo(methodOn(EmployeeController.class).
                                all()).withRel("employees")))
                .collect(Collectors.toList());

        return CollectionModel.of(employees, 
                linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
    }

这里的处理方法与前面类似,给每个雇员添加一个自链接,顶层封装聚合根。
重新启动应用,可以看到如下结果:
hateoas聚合根
集合的每个成员都有他们的信息以及相关链接。
添加所有这些链接有什么意义呢?它使 REST 服务能够随着时间的推移而发展。可以维护现有链接,将来可以添加新的链接。新客户可能会利用新链接,而老客户可以在旧链接上维护。如果服务被迁移动,这尤其有益。只要保持链接结构,客户仍然可以找到它并与之互动。

简化链接创建

在前面的代码中,单个雇员链接创建有重复?向雇员提供单个链接,同时创建指向聚合根的“employees”链接,显示了两次。如果这引起了你的关注,这里有一个解决方案。
简单地说,需要定义一个将 Employee 对象转换为 EntityMode l< Employee > 对象的函数。虽然可以自己编写此方法代码,但 Spring HATEOAS 的 RepresentationModelAssembler 接口将完成这项工作,代码如下:

package cn.lut.curiezhang.payroll;

import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: EmployeeModelAssembler</p>
 * <p>Description: </p>
 *
 * @author Curie Zhang
 * @version EmployeeModelAssembler.java v1.0 2021/7/12 10:29 curiezhang
 */
@Component
public class EmployeeModelAssembler implements
        RepresentationModelAssembler<Employee, EntityModel<Employee>> {
    /**
     * 将 Employee 对象转换为 EntityModel<Employee> 对象
     * @param employee Employee对象
     * @return EntityModel<Employee> 实体对象
     */
    @Override
    public EntityModel<Employee> toModel(Employee employee) {

        return EntityModel.of(employee, //
                linkTo(methodOn(EmployeeController.class)
                        .one(employee.getEmployId())).withSelfRel(),
                linkTo(methodOn(EmployeeController.class)
                        .all()).withRel("employees"));
    }
}

前面控制器中的类似处理都以及提取出来,移到这个类中,通过应用 Spring 框架的 @Component 注解,组装程序将在应用启动时自动创建。
要使用组装器,需要在控制器中注入,代码如下(只有这部分代码):

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

方法类似,这里可以使用 map(assembler::toModel) 替换 EntityModel< Employee > 的创建,使用 Java 8 的流可以很方便地插入到控制器中并简化它。
现在,已经创建了一个 Spring MVC REST 控制器,该控制器实际生成超媒体驱动的内容!不支持 HAL 的客户端可以在使用纯数据时忽略附加的位。支持 HAL 的客户端可以浏览授权的 API。但这并不是用 Spring 构建真正的 REST 服务的全部。

REST API 演化

通过添加一个库和几行额外的代码,可以在应用程序中添加超媒体。但这不是 REST 服务的全部。REST 的一个重要方面是,它既不是一个技术栈,也不是一个简单的标准。
REST 是一系列架构约束,当使用时会使应用程序更具弹性。弹性的一个关键因素是,当升级服务时,客端户不会遭受宕机的影响。
早期,升级会对客户造成破坏而臭名昭著。换句话说,服务器升级需要客户端进行更新。在当今这个时代,升级所花费的停机时间可能损失数百万美元。
有些公司如果要您向管理层提交一个尽量减少停机时间的计划。过去,您可以在负载最低的星期日凌晨 2 点进行升级。但是,在当今以互联网为基础的电子商务中,与其他时区的国际客户合作,这种策略并不凑效。
基于 SOAP 的服务和基于 CORBA 的服务是非常脆弱的。很难推出能够支持新旧客户端的服务。而采用基于 REST 的实践,就要容易得多。特别是使用 Spring 技术。

支持 API 变更

想象一下这样一个设计问题:您推出了一个基于 Employee 记录的系统,这个系统遭遇到了巨大打击,你已经把你的系统卖给了无数的企业,突然之间,雇员姓名需要分割成姓和名两部分。
哦,没想到会这样。
在你打开 Employee 类,用 firstName 和 lastName 来取代 name,停下来考虑一会。这会破坏客户端吗?升级它们需要多长时间?您是否可以控制所有访问服务的客户端?
停机时间=亏损,管理层准备好了吗?
在 REST 之前,有一句老话,“切勿删除数据库中的列”。您可以随时将列(字段)添加到数据库表中,但不要拿走一列。服务中的原则是相同的。
在 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 的?虽然这里有重复的信息,目的是支持新旧客户端。这意味着可以在不要求客户端升级的情况下升级服务器,这样应该能减少停机时间。现在,不仅能以"旧方式"和"新方式"显示信息,还应该双向处理传入的数据。下面就看看如何处理"旧"和"新"的客户端的雇员记录,在 Employee 类中添加代码如下:

    private String firstName;
    private String lastName;
    
    public Employee(String firstName, String lastName, String role) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.role = role;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
    public String getName() {
        return firstName + " " + lastName;
    }

    public void setName(String name) {
        String[] parts = name.split(" ");
        this.firstName = parts[0];
        this.lastName = parts[1];
    }

这个类和以前的 Employee 版本很相似。下面就来看一下其中的变化:

  • 字段 name 可以被替换为 firstName 和 lastName,如果 name 字段其他地方也在使用,可以新添加 firstName 和 lastName。
  • 定义了旧属性的"虚拟" getter 方法。它使用 firstName 和 lastName 字段生成 name 值。
  • 还定义了旧属性的"虚拟" setter 方法。它解析传入的 name 字符串,并存储到适当的字段。

当然,并不是对 API 的每一次变更都像拆分字符串或合并两个字符串一样简单。当然,对于大多数场景,这是可行的。

正确响应

现在要确保每个 REST 方法都返回适当的响应。更新 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);
    }
  • 新的 Employee 对象与以前一样保存起来,结果对象使用.EmployeeModelAssembler 进行封装
  • Spring MVC 用 ResponseEntity 建立一个 HTTP 201 创建状态消息。此类响应类型通常包括位置响应头,和从模型的自链接中派生的 URI。
  • 返回已保存对象的基于模型的版本。

通过这些改进,可以使用相同的端点创建新的雇员信息,并使用传统的 name 字段,如下图所示:
旧字段访问
这不仅在 HAL 中呈现了结果对象( name 以及 firstName / lastName),而且还用 http://localhost:8080/employees/646 填充位置头。超媒体支持的客户端可以选择"冲浪"到这个资源,并与它互动。
PUT 控制器方法需要类似的调整,如下所示:

    @PutMapping("/employees/{id}")
    ResponseEntity<?> replaceEmployee(@RequestBody Employee newEmployee,
                                      @PathVariable Long id) {

        Employee updatedEmployee = repository.findById(id) //
                .map(employee -> {
                    employee.setName(newEmployee.getName());
                    employee.setRole(newEmployee.getRole());
                    return repository.save(employee);
                }) //
                .orElseGet(() -> {
                    newEmployee.setEmployId(id);
                    return repository.save(newEmployee);
                });

        EntityModel<Employee> entityModel = assembler.toModel(updatedEmployee);

        return ResponseEntity //
                .created(entityModel
                        .getRequiredLink(IanaLinkRelations.SELF).toUri()) //
                .body(entityModel);
    }

这里,使用 save() 操作创建 Employee 对象,然后用 EmployeeModelAssembler 组装器封装到 EntityModel < Employee > 对象中。使用 getRequiredLink() 方法,可以检索由 EmployeeModelAssembler 创建的带有自链接的 Link,该方法使用 toUri 方法转换成一个 URI 并返回 Link。
要想有一个比 200 OK更详细的 HTTP 响应代码,可以使用 Spring MVC 的 ResponseEntity 封装器。它有一个静态方法 created(),可以建立资源的URI。PUT 操作如下图所示:
put操作
该雇员资源现已更新,URI 的位置已发回。最后,更新删除操作,处理删除请求,代码如下:

    @DeleteMapping("/employees/{id}")
    ResponseEntity<?> deleteEmployee(@PathVariable Long id) {
        Optional<Employee> rec = repository.findById(id);
        if (rec.isPresent()) {
            repository.deleteById(id);
            return ResponseEntity.accepted().body("Success");
        } else {
            return ResponseEntity.notFound().build();
        }
    }

如果操作成果,则返回”Success“,否则返回 404 错误,如下图所示:
delete
404错误
现在就可以升级啦,不会干扰现有客户端,而且新的客户端可以利用增强功能!顺便说一下,你担心会发送太多信息吗?在每个节都很重要的系统中,API 的演变可能要退居二线。但是,在度量之前,不要追求这种过早的优化。

构建指向 REST API 的链接

现在,已经建立了一个可演化的、具有最基本内容链接的 API。要想增强 API 并更好地为客户服务,需要采纳 HATEOAS(Hypermedia as the Engine of Application State)的概念。
什么意思呢?这里将进行详细的探讨。
业务逻辑不可避免地会建立涉及流程规则,系统的风险在于,将服务器端逻辑引入客户端并建立强耦合,REST 就是要打破这种联系,尽量减少这种耦合。下面,展示如何在不触及客户端重大变更的情况下处理状态改变,想象一下向系统中添加一个订单处理。
首先,定义 Order 记录,代码如下:

package cn.lut.curiezhang.order;
import java.util.Objects;

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

/**
 * <p>Package: cn.lut.curiezhang.order</p>
 * <p>Class Name: Order</p>
 * <p>Description: </p>
 *
 * @author Curie Zhang
 * @version Order.java v1.0 2021/7/13 9:46 curiezhang
 */
@Entity
@Table(name = "CUSTOMER_ORDER")
public class Order {
    private @Id @GeneratedValue Long id;

    private String description;
    private Status status;

    public Order() {}

    public Order(String description, Status status) {
        this.description = description;
        this.status = status;
    }

    public Long getId() {
        return this.id;
    }

    public String getDescription() {
        return this.description;
    }

    public Status getStatus() {
        return this.status;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    @Override
    public boolean equals(Object o) {

        if (this == o)
            return true;
        if (!(o instanceof Order))
            return false;
        Order order = (Order) o;
        return Objects.equals(this.id, order.id) 
                && Objects.equals(this.description, order.description)
                && this.status == order.status;
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.id, this.description, this.status);
    }

    @Override
    public String toString() {
        return "Order{" + "id=" + this.id + ", description='" 
                + this.description + '\'' + ", status=" + this.status + '}';
    }
}

从客户提交订单并完成或取消订单时起,订单经历一系列状态转换,这可以用 enum 来描述,代码如下:

package cn.lut.curiezhang.order;

/**
 * <p>Package: cn.lut.curiezhang.order</p>
 * <p>Class Name: Status</p>
 * <p>Description: </p>
 *
 * @author Curie Zhang
 * @version Status.java v1.0 2021/7/13 9:49 curiezhang
 */
public enum Status {
    IN_PROGRESS, // 正在处理
    COMPLETED, // 已完成
    CANCELLED // 已取消
}

要支持与数据库中的订单交互,必须定义相应的 Spring 数据存储库,使用 Spring Data JPA 的基本接口 JpaRepository,代码如下:

package cn.lut.curiezhang.order;

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

/**
 * <p>Package: cn.lut.curiezhang.order</p>
 * <p>Class Name: OrderRepository</p>
 * <p>Description: </p>
 *
 * @author Curie Zhang
 * @version OrderRepository.java v1.0 2021/7/13 9:58 curiezhang
 */
public interface OrderRepository extends JpaRepository<Order, Long> {
}

有了这个存储库,和 employee 类似,就可以定义一个基本的 OrderController,代码如下:

package cn.lut.curiezhang.order;

import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.mediatype.problem.Problem;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

/**
 * <p>Package: cn.lut.curiezhang.order</p>
 * <p>Class Name: OrderController</p>
 * <p>Description: </p>
 * 订单控制器
 *
 * @author Curie Zhang
 * @version OrderController.java v1.0 2021/7/13 10:04 curiezhang
 */
@RestController
public class OrderController {
    private final OrderRepository orderRepository;
    private final OrderModelAssembler assembler;

    OrderController(OrderRepository orderRepository,
                    OrderModelAssembler assembler) {

        this.orderRepository = orderRepository;
        this.assembler = assembler;
    }

    @GetMapping("/orders")
    CollectionModel<EntityModel<Order>> all() {
        List<EntityModel<Order>> orders = orderRepository.findAll().stream() //
                .map(assembler::toModel) //
                .collect(Collectors.toList());

        return CollectionModel.of(orders, //
                linkTo(methodOn(OrderController.class).all()).withSelfRel());
    }

    @GetMapping("/orders/{id}")
    EntityModel<Order> one(@PathVariable Long id) {
        Order order = orderRepository.findById(id) //
                .orElseThrow(() -> new OrderNotFoundException(id));

        return assembler.toModel(order);
    }

    @PostMapping("/orders")
    ResponseEntity<EntityModel<Order>> newOrder(@RequestBody Order order) {
        order.setStatus(Status.IN_PROGRESS);
        Order newOrder = orderRepository.save(order);

        return ResponseEntity //
                .created(linkTo(methodOn(OrderController.class)
                        .one(newOrder.getId())).toUri()) //
                .body(assembler.toModel(newOrder));
    }

    @DeleteMapping("/orders/{id}/cancel")
    ResponseEntity<?> cancel(@PathVariable Long id) {
        Order order = orderRepository.findById(id) //
                .orElseThrow(() -> new OrderNotFoundException(id));

        if (order.getStatus() == Status.IN_PROGRESS) {
            order.setStatus(Status.CANCELLED);
            return ResponseEntity
                    .ok(assembler.toModel(orderRepository.save(order)));
        }

        return ResponseEntity //
                .status(HttpStatus.METHOD_NOT_ALLOWED) //
                .header(HttpHeaders.CONTENT_TYPE,
                        MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) //
                .body(Problem.create() //
                        .withTitle("方法不允许!") //
                        .withDetail("不能取消处于 " + order.getStatus()
                                + " 状态的订单!"));
    }

    @PutMapping("/orders/{id}/complete")
    ResponseEntity<?> complete(@PathVariable Long id) {
        Order order = orderRepository.findById(id) //
                .orElseThrow(() -> new OrderNotFoundException(id));

        if (order.getStatus() == Status.IN_PROGRESS) {
            order.setStatus(Status.COMPLETED);
            return ResponseEntity
                    .ok(assembler.toModel(orderRepository.save(order)));
        }

        return ResponseEntity //
                .status(HttpStatus.METHOD_NOT_ALLOWED) //
                .header(HttpHeaders.CONTENT_TYPE,
                        MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) //
                .body(Problem.create() //
                        .withTitle("方法不允许!") //
                        .withDetail("不能完成处于 " +
                                 order.getStatus() + " 状态的订单!"));
    }
}

同样地,创建一个组装器,代码如下:

package cn.lut.curiezhang.order;

import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

/**
 * <p>Package: cn.lut.curiezhang.order</p>
 * <p>Class Name: OrderModelAssembler</p>
 * <p>Description: </p>
 *
 * @author Curie Zhang
 * @version OrderModelAssembler.java v1.0 2021/7/13 10:05 curiezhang
 */
@Component
public class OrderModelAssembler
        implements RepresentationModelAssembler<Order, EntityModel<Order>> {
    @Override
    public EntityModel<Order> toModel(Order order) {
        // 无条件链接单一资源和聚合根
        EntityModel<Order> orderModel = EntityModel.of(order,
                linkTo(methodOn(OrderController.class)
                        .one(order.getId())).withSelfRel(),
                linkTo(methodOn(OrderController.class)
                        .all()).withRel("orders"));

        // 基于订单状态的条件链接
        if (order.getStatus() == Status.IN_PROGRESS) {
            orderModel.add(linkTo(methodOn(OrderController.class)
                    .cancel(order.getId())).withRel("cancel"));
            orderModel.add(linkTo(methodOn(OrderController.class)
                    .complete(order.getId())).withRel("complete"));
        }

        return orderModel;
    }
}

现在,数据库初始化操作中加入 order 数据初始化,代码如下:

package cn.lut.curiezhang.payroll;

import cn.lut.curiezhang.order.Order;
import cn.lut.curiezhang.order.OrderRepository;
import cn.lut.curiezhang.order.Status;
import org.springframework.context.annotation.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;

/**
 * <p>Package: cn.lut.curiezhang.payroll</p>
 * <p>Class Name: LoadDatabase</p>
 * <p>Description: </p>
 * 加载测试数据
 *
 * @author Curie Zhang
 * @version LoadDatabase.java v1.0 2021/7/11 11:06 curiezhang
 */
@Configuration
public class LoadDatabase {
    private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);

    @Bean
    CommandLineRunner initDatabase(EmployeeRepository employeeRepository, 
                                   OrderRepository orderRepository) {

        return args -> {
            log.info("加载 " + employeeRepository.save(new Employee("张", "三",
                    "经理")));
            log.info("加载 " + employeeRepository.save(new Employee("李", "四",
                    "主任")));
            
            employeeRepository.findAll().forEach(employee -> log.info("加载 " + employee));


            orderRepository.save(new Order("MacBook Pro", Status.COMPLETED));
            orderRepository.save(new Order("iPhone", Status.IN_PROGRESS));

            orderRepository.findAll().forEach(order -> {
                log.info("加载 " + order);
            });
        };
    }
}

重新启动应用,就可以查看订单信息,如下图所示:

orders
取消订单,如下图所示:
cancel
如果再次取消订单,将显示不允许该操作,如下图所示:
再次取消

总结

这里介绍了各种构建 REST API 的策略。事实证明,REST 不仅仅是优雅的 URI,而且可返回 JSON,而不是XML。下面的策略有助于降低服务中断:

  • 不要移除旧字段,应该提供支持。
  • 使用基于关系的链接,客户端就不必硬编码 URI。
  • 尽可能长地保留旧链接。即使必须更改 URI,请保留关系,以便老的客户端能够使用新功能。
  • 使用链接(而非有效载荷数据)指引客户进行各种状态操作。

为每个资源类型构建 RepresentationModelAssembler 实现并在控制器中使用这些组件,这样,服务器端设置因为 Spring HATEOAS 而变得简单易用,可以确保受控的和非受控的客户端可以随着 API 的演化而轻松升级。
这里展示了如何使用 Spring 构建 REST 服务。

  • 非 rest 的——简单的 Spring MVC 应用程序,没有超媒体
  • rest 的——Spring MVC+Spring HATEOAS 应用程序与每个资源的 HAL 表示
  • 演化——REST 应用程序,其中一个字段发生变化,但保留旧数据提供为向后兼容性
  • 链接——REST 应用程序,使用有条件链接向客户端发送有效状态更改信号

这是整个项目的内容。

参考

Building REST services with Spring

下载

源代码 poy-roll.zip

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个用于构建独立的、生产级别的Spring应用程序的框架。而Apache CXF是一个开源的、全功能的Web服务框架,可用于构建和部署SOAP和RESTful Web服务。 在Spring Boot中整合CXF REST服务的步骤如下: 1. 添加CXF和Spring Boot的相关依赖。在pom.xml文件中添加以下依赖: ```xml <dependencies> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-spring-boot-starter-jaxrs</artifactId> <version>3.3.7</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.5.0</version> </dependency> </dependencies> ``` 2. 创建一个RESTful Web服务接口。在Spring Boot项目中创建一个标注有`@Path`注解的接口,定义RESTful风格的服务接口。 ```java @Path("/users") public interface UserService { @GET @Path("/{id}") @Produces(MediaType.APPLICATION_JSON) User getUser(@PathParam("id") String id); @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) User createUser(User user); // ... } ``` 3. 实现RESTful接口。创建一个实现了上述接口的类,并添加`@Service`注解,将其作为Spring的Bean进行管理。 ```java @Service public class UserServiceImpl implements UserService { @Override public User getUser(String id) { // 实现具体的逻辑 } @Override public User createUser(User user) { // 实现具体的逻辑 } // ... } ``` 4. 配置CXF。在application.properties(或application.yml)文件中添加以下配置: ```properties cxf.path=/api cxf.jaxrs.classes-scan=true ``` 这将配置CXF的访问路径和自动扫描RESTful服务接口的功能。 5. 启动Spring Boot应用程序。可以通过`@SpringBootApplication`注解标注主类,并添加`@Endpoint`注解来启动应用程序和发布Web服务。 至此,我们已经成功地将CXF REST服务整合到Spring Boot应用程序中。可以通过访问`http://localhost:8080/api/users/1`来调用RESTful接口的`getUser`方法,并得到返回结果。 总的来说,整合Spring Boot和CXF REST服务相对简单,只需添加相关依赖、定义接口和实现类,然后配置CXF即可。这种整合方式可以使开发者更加便捷地构建和部署RESTful Web服务

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值