CUBA 应用程序中的 Rest API

在本指南中,您将学习如何在 CUBA 应用程序中创建 REST API 。我们会为宠物诊所应用创建一些 API ,第三方应用可以通过这些 API 来控制 Petclinic 系统的就诊记录,以此学习 CUBA 应用程序中不同类型的 REST API 。

将要构建的内容

本指南对 CUBA Petclinic 示例进行了增强,以允许第三方平板应用通过安全的 API 访问就诊信息。通过 API 也可以开始、结束诊疗。

开发环境要求

您的开发环境需要满足以下条件:

下载 并解压本指南的源码,或者使用 git 克隆下来:

概述

API 是机器间进行交互常见方式,CUBA 基于 Spring Framework 提供各种形式的 API 技术。Spring 原生提供了对 REST 和 SOAP-Webservice API 的支持。另外,丰富的 Java 生态系统也提供了诸如 GraphQL 之类新的技术。

CUBA 提供了一些通用 REST API ,通过这些 API 提供的端点可以管理所有应用程序模型 。

在本指南中,我们将创建多个基于 REST 的 HTTP API,以允许其他应用程序与 CUBA Petclinic 进行交互。我们会在示例中同时使用通用 API 和自定义 HTTP 端点,以展示这两种方式之间的差异。

什么是 REST API

在研究用于提供通用 API 的 CUBA 实现之前,我们先简单了解一下 Rest API 的直观形态及其背后的概念。

REST API 是在 HTTP 协议之上实现的一些特殊的用法。REST 具有几个构建块(block),我们将在本指南中使用它们。

一般来说,API 是一种机制,用于不同的系统间的通信和数据交换。在这里我们将 REST 的概念定义为一种特定格式的 API,包括了资源、动词及请求-响应。

资源

您可以将资源视为对任何类型信息的概念抽象。比如,资源可以是图像、文档或记录。也可以是一只宠物,但不能是实际的宠物。在宠物诊所应用程序中,资源是真实宠物的虚拟化表示。资源使用被称为资源标识符的东西来标识。资源标识符的表现形式类似于一个 URL ,用于标识资源,比如: https://petclinic.cuba-platform.com/pets/123, 表示身份号为 “123” 的宠物。

资源动词

资源动词表示您可以对特定资源执行的 “操作”。在 REST 中,这些动词应形成统一的接口,这意味着该协议提供了一组有限的标准操作。在 HTTP 中,可以使用几个动词来与资源交互:

HTTP 中定义了几个可使用的动词。尽管它们的解释和用法通常取决于 API 的提供者,但它们都有特定的含义。

通常,动词具有与之相关以含义和承诺。例如,动词 GET 通常用于加载数据。根据 HTTP 规范,GET 操作是 幂等安全 的。这意味着 API 中的操作不应有副作用。多次执行相同的操作的结果都应相同。通常,多次加载一个宠物的信息不应该产生副作用。因此,将 GET 动词用于此操作与 GET 动词所提供的承诺相匹配。

另一方面,两次注册 Pet 将导致在应用程序中存储两个 Pet。因此,此操作并非没有副作用。对于这种类型的操作,POST 动词是一个更好的选择,因为该动词不保证无副作用。

HTTP 协议定义了多个动词。主要有:GETPOSTPUTDELETEPATCH

请求和响应

REST 中并未定义请求和响应的概念,但它们基于 HTTP 协议。请求和响应是客户端和服务器之间发生的实际 HTTP 交互。请求是从客户端到服务器的初始消息。响应是从服务器返回到客户端的结果消息。两种消息通常都包含两个部分:消息有效负载和消息元数据。

这里是一个 HTTP 请求和响应消息的示例:

创建就诊记录(Visit)的 HTTP 请求消息示例
POST http://petclinic.cuba-platform.com/rest/api/visits (1)
Authorization: Bearer 71dbb8a8-2a41-45e7-a73b-16a96c433651
Content-Type: application/json (2)

{ (3)
  "petIdentificationNumber": "025",
  "visitStart": "2020-04-05T08:00:00",
  "visitEnd": "2020-04-05T10:00:00",
  "type": "REGULAR_CHECKUP",
  "description": "This is a regular checkup for Pikachu"
}
1 动词+资源标识符,表示要执行的操作
2 Content-Type 指示服务器应如何解释消息的有效负载
3 请求的有效负载- 正文包含了业务相关的信息

服务器收到 HTTP 请求后,将生成以下响应:

表示就诊记录创建成功的 HTTP 响应:
HTTP/1.1 200 (1)
Content-Type: application/json

{ (2)
  "id": "4e3bde19-c0ec-7cd0-654a-577ba32dcc7f"
}
1 200 响应代码,对交互结果进行分类
2 响应消息(正文)包含实际的业务相关信息(这里为新建就诊记录的 ID)

如上所述,API 交互的消息有效负载通常使用 JSON 格式。但是,也可以通过 HTTP 使用其它格式的消息,例如 XMLwww-url-formencoded 。 同样,也可以是二进制格式(用于传输PDF文档/图像)。

有了这些基础信息,我们现在可以着重介绍 CUBA 应用程序中 REST API。

如何创建 HTTP Controller

在下一部分中,我们将研究 CUBA 应用程序中 HTTP 的具体用法。CUBA 提供了一些通用的 REST API。CUBA 基于 Spring MVC,我们可以使用 Spring MVC 中的控制器提供自定义 Rest API。

要创建一个 HTTP 端点,必须创建一个 Java 类并为其添加 @RestController 注解。要实现 API 操作 GET https://petclinic.cuba-platform.com/pets/123 , 需要使用 @GetMapping 注解一个方法,这个注解会将方法链接到指定端点。

这里是 Spring MVC 中 REST 控制器的示例:

FetchPetController.java
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController (1)
public class FetchPetController {

    @GetMapping("/pets/{petId}") (2)
    public ResponseEntity<Pet> fetchPet(
        @PathVariable("petId") String petId (3)
    ) {

        final Optional<Pet> possiblePet = dataManager.load(Pet.class)
            .query("e.identificationNumber = ?1", petId)
            .optional();

        if (!possiblePet.isPresent()) {
            return ResponseEntity.notFound().build();
        }

        return ResponseEntity.ok( (4)
            possiblePet.get()
        );
    }
}
1 将类注册为 HTTP/REST 控制器
2 fetchPet 方法通过 @GetMapping 注解链接到端点 GET/pets/{petId}
3 URL 的 petId 部分是动态的,会作为参数传递给方法
4 ResponseEntity 负责返回正确的 HTTP 状态代码和 HTTP 响应正文

对于不同的 HTTP 动词,Spring MVC 提供了相应的注解,我们会在后面的代码清单中看到。

HTTP 消息元数据中包含了响应代码。这些代码用于给客户端表示操作结果。响应代码 200 - OK 表示操作成功,而 404-Not Found 则告诉客户端请求的资源不可用。有几种响应代码可用于精确描述操作的结果。

ResponseEntity 负责控制返回给客户端的 HTTP 响应。但是它也用于定义响应主体。在上面的示例中,Pet 对象被视为响应主体。Spring MVC 负责将 Pet 对象转换为JSON格式,并将其返回给 HTTP 客户端。

宠物诊所应用中的 REST API

在本节中,我们将研究 CUBA Petclinic 应用程序,了解如何为该应用程序创建 REST API,以便其他应用程序与 Petclinic 进行交互。

在大多数示例中我们会利用 CUBA REST API 扩展组件。这个组件已经在示例项目中安装了。如果将示例作为自有实现的起点,请确保通过 CUBA Marketplace 将扩展组件添加到应用程序中。

可以在示例项目的 requests 目录中找到测试该解决方案的所有示例请求,此目录包含多个示例请求。

HTTP 请求示例
如果在 IntelliJ IDEA Ultimate 中使用 CUBA Studio,可以启动应用程序,打开 http 请求文件,然后点击请求旁边的 Run HTTP Request 按钮来执行请求。使用这个工具我们可以交互式地使用 API,并查看实际的 HTTP 请求和响应。

在宠物诊所应用中将有一个新的应用,名为:PetTreat , 兽医和护士可以在他们的平板电脑使用这个新应用。PetTreat 应用可辅助员工对宠物进行治疗。因此,平板电脑应用必须与 CUBA Petclinic 应用通信。在 PetTreat 应用中的数据更改都必须同步到 CUBA Petclinic 应用,以供所有员工使用。

在对功能进行了解后,可以标识出以下数据交互需求:

  • 按名称搜索宠物

  • 为宠物创建就诊就录

  • 查看已有的就诊录

  • 开始和结束诊疗

由于这些信息和操作非常敏感,因此仅允许指定的兽医和护士可以通过 PetTreatCUBA Petclinic 系统进行交互。将信息输入 PetTreat 后,该信息应立即提供给 Petclinic 的所有其他员工,因此 CUBA Petclinic 中的 API 将用于两个应用程序之间的交互。

CUBA 通过上述 Spring MVC API 和通用 REST API 扩展组件为这些需求提供解决方案。在此示例中,我们将研究这两种解决方案并比较它们的实现方式。

为了满足安全认证要求,我们将从了解 CUBA OAuth2 的实现开始,该实现是一种通过 API 对用户进行身份验证的机制。

CUBA OAuth2 API 认证

OAuth2 是用于为 API 交互提供身份验证机制的通用 Internet 标准。CUBA 通过 REST API 扩展组件 提供了开箱即用的 OAuth2 身份验证支持。通过 OAuth2 进行身份验证的方式如下:

  1. 客户端带上登录凭据(用户名和密码)发出请去获取 access token

  2. access token 是一个临时的凭证,用于验证后续的请求,后续的请求都要在 HTTP 头中带上这个 token

执行登录并获取访问令牌的请求如下所示:

HTTP Access Token Request
POST http://localhost:8080/app/rest/v2/oauth/token
Authorization: Basic {{cubaRestClientId}} {{cubaRestClientSecret}}
Content-Type: application/x-www-form-urlencoded

grant_type=password&username={{username}}&password={{password}}
花括号括住的部分是变量,例如 {{username}} ,将用实际值替换。例如,usernamepassword 需要使用用实际的系统用户名和密码替换。

如果提供了正确的凭据,则这个 HTTP POST 请会得到以下响应,其中包含了访问令牌:

HTTP Access Token 响应
{
  "access_token": "56b53dab-0988-4480-84ae-5690bdf713dd",
  "token_type": "bearer",
  "refresh_token": "44727aca-07d7-4363-b545-d2801502d01b",
  "expires_in": 43199,
  "scope": "rest-api"
}

access_token 值必须存储在客户端应用程序中,以用于后续的请求。完成身份验证步骤后,就可以以已认证的方式与 REST API 进行交互。这意味着使用此访问令牌执行的其他请求将使用与最初请求令牌时提供的身份一致。

这时,所有的请求的处理会完全处在 CUBA 安全子系统的控制下,角色、访问组的权限配置都会生效。例如,如果用户在一个访问组,而该访问组仅允许看到 Water 类的宠物,那么 API 可获取到的数据也在这个范围内。

您可以在扩展组件的文档中找到有关安全性的更多信息:

接下来我们来看看 PetTreat 应用的功能需求,并了解如何通过 API 执行相关操作。我们将从 REST API 扩展组件提供的通用 API 开始。

REST 扩展组件的通用 API

使用通用的 REST API 可以满足从 Pets 和 Visits 查询数据、创建就诊记录和启动诊疗的这四个需求。

在探索工作原理之前,让我们快速了解一下 REST API 扩展组件中通用 API 的概念。

借助扩展组件,CUBA 应用程序提供了一套 API,可用于对所有实体执行 CRUD 操作。无需在服务器端编写代码即可为实体数据提供 API。

正如我们将在示例中看到的一样,仅通过在配置文件中注册业务服务,就可以使用通用 API 将业务服务公布为 REST 服务。

获取就诊记录信息

要通过通用 API 检索就诊记录(Visit)信息,可以执行以下 HTTP GET 请求:

获取就诊记录信息
GET http://localhost:8080/app/rest/v2/entities/petclinic_Visit (1)
Authorization: Bearer {{auth_token}} (2)


HTTP/1.1 200
Content-Type: application/json;charset=UTF-8

[ (3)
  {
    "_entityName": "petclinic_Visit",
    "_instanceName": "198 - Murkrow ()",
    "id": "0019e1b3-ab9a-c322-ef9b-5824fab430c2",
    "visitEnd": "2020-09-07 10:00:00.000",
    "description": "Surgery",
    "type": "RECHARGE",
    "visitStart": "2020-09-07 08:30:00.000",
    "version": 1,
    "petName": "Murkrow",
    "treatmentStatus": "DONE",
    "typeStyle": "event-green",
    "pet": {
      "_entityName": "petclinic_Pet",
      "_instanceName": "198 - Murkrow",
      "id": "e66f31a9-fa41-3e76-e881-d139439afc27",
      "version": 1,
      "name": "Murkrow",
      "identificationNumber": "198"
    }
  },
  {
    "_entityName": "petclinic_Visit",
    "_instanceName": "118 - Goldeen ()",
    "id": "001c0a58-3e2d-7d11-e93f-a16e21208378",
    "visitEnd": "2019-10-21 12:45:00.000",
    "description": "Surgery",
    "type": "REGULAR_CHECKUP",
    "visitStart": "2019-10-21 12:15:00.000",
    "version": 1,
    "petName": "Goldeen",
    "treatmentStatus": "DONE",
    "typeStyle": "event-blue",
    "pet": {
      "_entityName": "petclinic_Pet",
      "_instanceName": "118 - Goldeen",
      "id": "ebc6b61a-e6cc-8b95-4139-cc76fd408539",
      "version": 1,
      "name": "Goldeen",
      "identificationNumber": "118"
    }
  },
  {
    "_entityName": "petclinic_Visit",
    "_instanceName": "109 - Smogon ()",
    "id": "00384f37-0361-56ba-1289-89827d682ce8",
    "visitEnd": "2020-09-04 14:30:00.000",
    "description": "Fever",
    "type": "RECHARGE",
    "visitStart": "2020-09-04 13:30:00.000",
    "version": 1,
    "petName": "Smogon",
    "treatmentStatus": "DONE",
    "typeStyle": "event-green",
    "pet": {
      "_entityName": "petclinic_Pet",
      "_instanceName": "109 - Smogon",
      "id": "4100306c-49cd-0961-bfa3-c50859f1e6c4",
      "version": 1,
      "name": "Smogon",
      "identificationNumber": "109"
    }
  }
]
1 URL path entities/petclinic_Visit 指示要加载的实体数据
2 必须在请求中带上访问令牌以表明身份
3 响应中包含了所有就诊记录及相关数据

根据名称过滤宠物

通用 API 还提供了执行实体搜索的端点。可以定义过滤条件,类似于在通用过滤器组件中执行的操作。可以通过以下方式过滤名称为 "Pikachu" 的宠物:

根据宠物名称搜索
POST http://localhost:8080/app/rest/v2/entities/petclinic_Pet/search
Authorization: Bearer {{auth_token}}

{
  "filter": { (1)
    "conditions": [
      {
        "property": "name",
        "operator": "=",
        "value": "Pikachu"
      }
    ]
  },
  "view": "pet-with-owner-and-type" (2)
}


HTTP/1.1 200
Content-Type: application/json;charset=UTF-8

[ (3)
  {
    "_entityName": "petclinic_Pet",
    "_instanceName": "025 - Pikachu",
    "id": "d83cc7f7-69b5-3830-ff1d-ed74d1e4a79c",
    "owner": {
      "_entityName": "petclinic_Owner",
      "_instanceName": "Ash Ketchum",
      "id": "351eb2d2-c70b-3af7-109c-2b19a5929101",
      "lastName": "Ketchum",
      "address": "Miastreet 134",
      "city": "Alabastia",
      "telephone": "00497688166348",
      "firstName": "Ash",
      "email": "ash-ketchum@example.com"
    },
    "type": {
      "_entityName": "petclinic_PetType",
      "_instanceName": "Electric",
      "id": "d390dc26-3462-7586-221a-3110f0fcd97c",
      "name": "Electric"
    },
    "birthDate": "1998-01-03",
    "name": "Pikachu",
    "identificationNumber": "025"
  }
]
1 过滤条件的定义在 HTTP POST 正文中发送
2 可以指定一个视图来定义返回哪些数据
3 HTTP 响应中包含了与搜索条件匹配的条目列表

使用这种非常灵活的 API,可以对实体列表执行各种查询。可以在 文档 中找到更多信息。通过这两个端点,API 可以满足在 PetTreat 应用程序中查看和搜索各种 Visit 和 Pet 数据的要求。

根据身份号查找宠物

另一个类似的示例是通过身份号找到宠物。这也可以通过搜索 API 来实现,但是在这里,让我们看一下如何使用自定义服务来提供此功能。示例应用程序中的 PetService 服务有一个 findById(String petIdentificationNumber) 方法,该方法返回一个 Pet 实例。可以直接通过通用 REST API 公布此服务,以便可以通过 API 调用该方法。

为此,我们需要在 Web 模块中名为 rest-services.xml 的配置文件中注册 Service 方法。

rest-services.xml
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">

    <service name="petclinic_PetService">
        <method name="findById"> (1)
            <param name="petIdentificationNumber"/>
        </method>
    </service>

</services>
1 方法 findById 被标记为通用 REST API,通过验证的用户可以访问此 API

该文件还必须通过 web-app.properties 附加到配置中,如下所示:

cuba.rest.servicesConfig=+com/haulmont/sample/petclinic/rest-services.xml
在 CUBA Studio 中,切换到 CUBA 项目视图,在 "REST API" 点击右键,再点击 ”Create REST Services Configuration“ 可自动生成配置文件并和在 web-app.properties 中注册。

既然已经公开了该方法,则可以通过以下方式与该方法进行交互:

创建就诊记录(Visit)
GET http://localhost:8080/app/rest/v2/services/petclinic_PetService/findById?petIdentificationNumber=025 (1)
Authorization: Bearer {{auth_token}}


HTTP/1.1 200
Content-Type: application/json;charset=UTF-8

{ (2)
  "_entityName": "petclinic_Pet",
  "id": "d83cc7f7-69b5-3830-ff1d-ed74d1e4a79c",
  "owner": {
    "id": "351eb2d2-c70b-3af7-109c-2b19a5929101",
    "name": "Ash Ketchum",
  },
  "type": {
    "_entityName": "petclinic_PetType",
    "_instanceName": "Electric",
    "id": "d390dc26-3462-7586-221a-3110f0fcd97c",
    "name": "Electric",
    "version": 1
  },
  "birthDate": "1998-01-03",
  "name": "Pikachu",
  "identificationNumber": "025"
}
1 通过 HTTP GET 调用方法,方法参数使用 URL 参数 ?petIdentificationNumber=025 传入
2 响应主体是 JSON 格式,包含了 Pet 信息

为指定宠物创建就诊记录

也可以通过通用 Entities API 创建就诊记录。要创就诊记录,必须执行以下 HTTP POST 请求:

创建就诊记录
POST http://localhost:8080/app/rest/v2/entities/petclinic_Visit
Authorization: Bearer {{auth_token}}

{ (1)
    "visitStart": "2020-10-14 08:30:00.000",
    "visitEnd": "2020-10-15 10:00:00.000",
    "description": "Surgery created via API",
    "type": "RECHARGE",
    "pet": {
      "id": "d83cc7f7-69b5-3830-ff1d-ed74d1e4a79c" (2)
    }
}


HTTP/1.1 201 (3)
Location: http://localhost:8080/petclinic/rest/v2/entities/petclinic_Visit/a1aa87cb-c68f-ef2f-a69a-a685eeb2dd0c
Content-Type: application/json;charset=UTF-8

{
  "_entityName": "petclinic_Visit",
  "id": "a1aa87cb-c68f-ef2f-a69a-a685eeb2dd0c" (4)
}
1 在请求正文中发送就诊信息
2 使用 ID 指向其他实体(如 Pet )
3 对于有效请求,HTTP响应是 201 - Created
4 结果包含新建的 Visit 的 ID

在实体中使用 Bean 验证时,将对请求检查约束。如果发送了无效数据,将不会创建就诊记录,并且客户端将得到一条错误消息,其中包含有关错误原因的信息。

开始和结束诊疗

PetTreat 需要执行的最后一个 API 操作是开始/结束诊疗。在实体模型中,此操作通过 Visit 实体中名为 treatmentStatus 的状态字段表示。

以下请求将标记诊疗开始:

开始诊疗
PUT http://localhost:8080/app/rest/v2/entities/petclinic_Visit/{{visitId}} (1)
Authorization: Bearer {{auth_token}}

{
    "treatmentStatus": "IN_PROGRESS" (2)
}


HTTP/1.1 200
1 通过 Visit 的 URL 执行 HTTP PUT 请求
2 要更新的字段通过请求正文发送

可以使用相同的 HTTP 请求完成处理,但是必须在字段值中发送 DONE,而不是发送 IN_PROGRESS

自定义 API 操作

在了解了如何利用 CUBA 的通用 REST API 之后,让我们了解如何通过 Spring MVC 的 REST 控制器来创建自定义 API,如开头所示。让我们上面四个场景如何使用自定义 API 满足。

在 CUBA 中注册自定义控制器

第一步是创建一个 Spring MVC 控制器类,并在 CUBA 应用程序中注册。要进行注册,我们需要在 web 模块的 com/haulmont/sample/petclinic 包下创建一个名为 rest-dispatcher-spring.xml 的文件。它包含自定义 API 的配置:

rest-dispatcher-spring.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
      xmlns:security="http://www.springframework.org/schema/security"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-4.3.xsd">


  <context:component-scan
    base-package="com.haulmont.sample.petclinic.web.controller" (1)
  />

  <security:http
    pattern="/rest/api/**" (2)
    create-session="stateless"
    entry-point-ref="oauthAuthenticationEntryPoint"
    xmlns="http://www.springframework.org/schema/security">
    <!-- Specify one or more protected URL patterns-->
    <intercept-url
        pattern="/rest/api/**"
        access="isAuthenticated()"
    />
    <anonymous enabled="false"/>
    <csrf disabled="true"/>
    <cors configuration-source-ref="cuba_RestCorsSource"/>
    <custom-filter ref="resourceFilter" before="PRE_AUTH_FILTER"/>
    <custom-filter ref="cuba_AnonymousAuthenticationFilter" after="PRE_AUTH_FILTER"/>
  </security:http>

</beans>
1 激活自动搜索 controller 子包中带有 @Controller 注解的类
2 为路由 /rest/api/** 以及以此为前缀的所有控制器配置身份验证

创建此文件后,需要在 web-app.properties 中进行注册,如下所示:

web-app.properties
cuba.restSpringContextConfig = +com/haulmont/sample/petclinic/rest-dispatcher-spring.xml

有了这个配置,我们可以在 controller 包中创建我们的第一个控制器。

获取宠物和就诊信息

PetTreat 要求的第一个自定义 API 是加载 Pet 和 Visit 信息以便显示。对于 fetchPet 控制器,我们已经在上面看到了一个示例。对于 Visit 的获取,不同之处在于使用了专门的响应对象:

VisitController.java
@RestController(VisitController.NAME)
@RequestMapping("/api")
public class VisitController {

    // ...

    @Inject
    protected VisitService visitService;

    @GetMapping("/visits/{visitId}")
    public ResponseEntity<FetchVisitApiResponse> fetchVisit(
        @PathVariable("visitId") UUID visitId
    ) {

        final Optional<Visit> visit = Optional.ofNullable(
            visitService.fetch(visitId)
        ); (1)

        return ResponseEntity.of(
            FetchVisitApiResponse.of(visit) (2)
        );
    }

}
1 通过 VisitService 加载 Visit 实体,该 VisitService 从数据库加载 Visit
2 将 Visit 转换为 FetchVisitApiResponse 对象,该对象作为响应主体发送

这里有两个有趣的地方,需要进一步阐述。 首先,控制器的功能范围很小。尽管可以直接对数据库执行数据提取,也可以进行数据转换,但是这些职责被置于不同的类中。保持控制器层轻薄,只让它负责单一职责:进行HTTP交互,实际的业务逻辑委托给其他类。通常这是一个好的做法。

这里要注意的第二件事是 FetchVisitApiResponse 对象。 此类定义了通过 HTTP 传输到客户端的 JSON 响应。它主要是一个数据传输对象(DTO),它将 HTTP 响应与应用程序的实体模型分离。

FetchVisitApiResponse.java
public class FetchVisitApiResponse {

    private final String id;
    private final String petIdentificationNumber;

    @JsonFormat(pattern = "dd/MM/yyyy") (1)
    private final LocalDateTime visitStart;

    @JsonFormat(pattern = "dd/MM/yyyy")
    private final LocalDateTime visitEnd;

    private final ApiVisitType type; (2)
    private final String description;

    // ...

    public static Optional<FetchVisitApiResponse> of(Optional<Visit> possibleVisit) {
        return possibleVisit.map(visit -> (3)
            new FetchVisitApiResponse(
                visit.getId().toString(),
                visit.getPet().getIdentificationNumber(),
                visit.getVisitStart(),
                visit.getVisitEnd(),
                ApiVisitType.ofEntityType(visit.getType())
                    .orElse(null),
                visit.getDescription()
            )
        );
    }

    public String getPetIdentificationNumber() { (4)
        return petIdentificationNumber;
    }

    public LocalDateTime getVisitStart() {
        return visitStart;
    }

    // ...
}
1 Jackson 注解用于配置特定字段的 JSON 输出
2 与实体层相比,这些字段可包含不同的类型(如 ApiVisitType 而不是 VisitType
3 of 工厂方法将实体层映射到 DTO 层,并在需要时转换属性
4 getter 方法定义哪些属性将通过 JSON 公开

该类主要作用在于,您可以精确控制通过 API 显示哪些属性。此外,由于 JSON 响应与实体模型解耦,因此可以在保持 API 约定完整的同时调整实体层的结构。如果您决定重命名你的实体属性,这时不需要 API 用户同步修改源码。

为宠物创建就诊就录

下一个操作是为宠物创建就诊记录。该端点由 VisitController 中的另外一个方法定义:

VisitController.java
public class VisitController {

    @Inject
    protected VisitService visitService;

    @PostMapping("/visits")
    public ResponseEntity<CreateVisitApiResponse> createUpcomingVisit(
        @RequestBody @Valid CreateVisitApiRequest createVisitApiRequest (1)
    ) {

        final Visit visit = visitService.create(
            createVisitApiRequest.toVisitCreation() (2)
        );

        return ResponseEntity.ok(
            CreateVisitApiResponse.of(visit) (3)
        );
    }
}
1 从请求的 JSON 中解析出 CreateVisitApiRequest 参数
2 VisitService 接收一个 VisitCreation 对象,并且不直接依赖于请求对象
3 创建就诊记录后,将向客户端返回一个 CreateVisitApiResponse 对象

在此示例中,我们将 Petclinic 的内部实现与外部世界分离。使用专门的 API 对象 CreateVisitApiRequestCreateVisitApiResponse 来表示与 API 客户端的约定。这些类仅在控制器层中使用。在内部,当与 VisitService 交互时,请求对象将转换为另一个名为 VisitCreation 的 DTO。此类位于 CUBA 的全局模块中。这将在 REST API 和内部服务层之间实施适当的分层。

request 参数上的 @Valid 注解指示 Spring 需要通过 bean 验证来验证请求对象。如果 JSON 请求包含无效数据,Spring将返回正确的错误响应,并带有一条有关 bean 验证结果的消息。

开始和结束诊疗

最后的业务需求是能够通过 PetTreat 开始和结束诊疗活动。该 API 与创建就诊记录的 API 相似,但是有一个有趣的区别:API 作为创建额外抽象的一种方式 ,到目前为止,API 或多或少都反映了 Petclinic 的领域模型。通常这就够用了,但是有时可以使用 API 层来简化内部域模型或隐藏某些细节。

首先让我们看一下控制器,以便您了解我们正在谈论的是哪种抽象:

TreatmentController.java
@RestController(TreatmentController.NAME)
@RequestMapping("/api")
public class TreatmentController {

    public static final String NAME = "petclinic_TreatmentController";

    @Inject
    protected TreatmentService treatmentService;

    @PostMapping("/treatments/{visitId}")
    public ResponseEntity<Void> startTreatment(
        @PathVariable("visitId") UUID visitId
    ) {
        treatmentService.start(visitId);
        return noContent();
    }

    @PatchMapping("/treatments/{visitId}")
    public ResponseEntity<Void> finishTreatment(
        @PathVariable("visitId") UUID visitId,
        @RequestBody @Valid FinishTreatmentApiRequest request
    ) {

        if (request.isCancelled()) {
            treatmentService.cancel(visitId);
        }
        else {
            treatmentService.finish(visitId);
        }

        return noContent();
    }

    private ResponseEntity<Void> noContent() {
        return ResponseEntity.noContent().build();
    }
}

控制器提供了两个端点:“开始诊疗”:POST/treatments/{visitId} 和 “结束诊疗”:PATCH/treatments/{visitId}。不同功能可以使用相同的 URL,但是使用不同的 HTTP Method。在这里,这样做的原因是这两个操作的对象是相同的。

API 在内部对 Visit 实体执行更改,主要是修改 treatmentStatus 属性。如果您回顾使用通用 API 的解决方案,您会看到我们直接更新了此字段。

在这个自定义 API 实现中,我们面向的对象不是 Visits,而是引入了 Treatment 的概念,围绕此概念提供了端点、设计了 request 和 response 对象。下面是结束就诊的 HTTP 请求示例:

Finish Treatment Request
PATCH http://localhost:8080/petclinic/rest/api/treatments/{{visitId}}
Authorization: Bearer {{auth_token}}
Content-Type: application/json

{
  "outcome": "DONE"
}

在这里,API 在 CRUD 操作的基础上为 Visit 领域模型提供了更高级别的抽象。FinishTreatmentApiRequest 包含一个确定处理结果的 outcome 属性,取值有 DONECANCELLED。这里没有使用枚举,只是使用了固定字符串来区分不同的操作。

不同的方式的优缺点

通过这些示例,我们了解了 CUBA 应用程序中的 REST API 的两实现用法。让我们看看这身份种 API 实现方法的优缺点。

通用 API

CUBA 提供的通用 REST API 可以节省大量时间。它开箱即用,对实体具有完整的 CRUD 操作。此外,它还实现了其他一些扩展功能,例如文件上传\Swagger API 文档、实体属性转换端点。

多种方式的实体加载功能、搜索功能和灵活的查询条件定义机制为客户端提供了极大的方便,所有这些功能不需要任何服务端开发。

通过使用不同的实体视图,还可以控制给客户端内返回的数据量。在上面的示例中,我们看到了用 pet-with-owner-and-type 视图调用搜索操作。在这里,不仅返回了宠物本身,而且也返回了宠物的主人信息、类型信息。

使用内置的安全性机制,可以透明地控制 API 客户端能够在服务器端检索哪些数据,就像在 UI 中一样。

通过将服务方法公开为 API,可以通过 API 公开业务逻辑,而无需在服务器端开发 HTTP 交互。这允许在直接实体访问之上创建抽象,以确保数据完整性以及后续的验证等。

自定义 API

自定义的 Petclinic API 可以利用 Spring MVC 提供的机制以非常精确的方式控制 HTTP 交互。可以定义输入数据以及输出数据。 HTTP 响应代码可用于描述 HTTP 生命周期状态,比如发生错误。

使用专门的请求和响应对象定义输入和输出数据,可以精确地控制返回的数据。此外,它允许在实体模型之上定义更高级别的 API 抽象。通常,最好不要让每个数据库实体都可以通过 API 访问。将实体模型和客户端 API 解耦,可以允许在不更改客户端 API 的情况下调整实体模型。

通用 API 提供了一些机制来克服这些缺点。 例如。可以定义 模型版本信息,这样在实体属性发生变化时可以提供向后兼容性。但是与控制 java 类中的输入和输出数据相比,这些措施有不可避免的局限性。直接调用 CUBA 服务是将实体模型与 API 解耦的一种可选的方法。

开发自定义 API 非常耗时,尤其是在需要大量 API 的情况下。但是,如果对 API 的向后兼容性要求很高,则必须使用自定义 API。

CUBA 提供了通用 API 机制,Spring MVC 提供了自定义 API 机制。根据具体情况可以选择使用哪种机制,也可以两种机制同时使用。比如,搜索功能使用通用 API ,写操作使用自定义 API 控制器。

总结

在本指南中,我们了解了 REST API 的基础架构风格及其在 HTTP 中的具体应用。我们讨论了诸如资源、动词以及请求和响应之类的基础知识。之后,我们重点学习了如何在 Spring 中创建 HTTP 端点。使用注解(@RestController@GetMapping)驱动的方式将类和方法绑定到 HTTP 端点。

了解了这些知识之后,我们开始研究 Petclinic 示例,以了解如何将虚构的 PetTreat 应用程序连接到该示例。在 CUBA 中,涉及 REST API 时有两个主要选项:使用 CUBA 提供的通用 API 或通过 Spring 自定义 REST API。 我们分别使用了这两种方式来解决 PetTreat 应用的各种功能需求。

在指南最后我们对两种方式进行了比较。如我们所见,通用 API 和自定义 API 都有其优缺点。CUBA 允许您灵活选择实现方式,以获得最佳解决方案。