使用 Resilience4j 框架实现重试机制


在本文中,我们将从快速介绍 Resilience4j 开始,然后深入探讨其 Retry 模块。我们将了解何时、如何使用它,以及它提供的功能。在此过程中,我们还将学习实现重试时的一些良好实践。

代码示例

本文在 GitHub 上附有工作代码示例。

什么是 Resilience4j?

当应用程序通过网络进行通信时,会有很多出错的情况。由于连接断开、网络故障、上游服务不可用等,操作可能会超时或失败。应用程序可能会相互过载、无响应甚至崩溃。

Resilience4j 是一个 Java 库,可以帮助我们构建弹性和容错的应用程序。它提供了一个框架,可编写代码以防止和处理此类问题

Resilience4j 为 Java 8 及更高版本编写,适用于函数接口、lambda 表达式和方法引用等结构。

Resilience4j 模块

让我们快速浏览一下这些模块及其用途:

模块 目的
Retry 自动重试失败的远程操作
RateLimiter 限制我们在一定时间内调用远程操作的次数
TimeLimiter 调用远程操作时设置时间限制
Circuit Breaker 当远程操作持续失败时,快速失败或执行默认操作
Bulkhead 限制并发远程操作的数量
Cache 存储昂贵的远程操作的结果

使用范式

虽然每个模块都有其抽象,但通常的使用范式如下:

  1. 创建一个 Resilience4j 配置对象
  2. 为此类配置创建一个 Registry 对象
  3. 从注册表创建或获取 Resilience4j 对象
  4. 将远程操作编码为 lambda 表达式或函数式接口或通常的 Java 方法
  5. 使用提供的辅助方法之一围绕第 4 步中的代码创建装饰器或包装器
  6. 调用装饰器方法来调用远程操作
    步骤 1-5 通常在应用程序启动时完成一次。让我们看看重试模块的这些步骤:
RetryConfig config = RetryConfig.ofDefaults(); // ----> 1
RetryRegistry registry = RetryRegistry.of(config); // ----> 2
Retry retry = registry.retry("flightSearchService", config); // ----> 3

FlightSearchService searchService = new FlightSearchService();
SearchRequest request = new SearchRequest("NYC", "LAX", "07/21/2020");
Supplier<List<Flight>> flightSearchSupplier =
  () -> searchService.searchFlights(request); // ----> 4

Supplier<List<Flight>> retryingFlightSearch =
  Retry.decorateSupplier(retry, flightSearchSupplier); // ----> 5

System.out.println(retryingFlightSearch.get()); // ----> 6

什么时候使用重试?

远程操作可以是通过网络发出的任何请求。通常,它是以下之一:

  1. 向 REST 端点发送 HTTP 请求
  2. 调用远程过程 (RPC) 或 Web 服务
  3. 从数据存储(SQL/NoSQL 数据库、对象存储等)读取和写入数据
  4. 向消息代理(RabbitMQ/ActiveMQ/Kafka 等)发送和接收消息

当远程操作失败时,我们有两种选择——立即向我们的客户端返回错误,或者重试操作。如果重试成功,这对客户来说是件好事——他们甚至不必知道这是一个临时问题。

选择哪个选项取决于错误类型(瞬时或永久)、操作(幂等或非幂等)、客户端(人或应用程序)和用例。

暂时性错误是暂时的,通常,如果重试,操作很可能会成功。请求被上游服务限制、连接断开或由于某些服务暂时不可用而超时就是例子。

来自 REST API 的硬件故障或 404(未找到)响应是永久性错误的示例,重试无济于事

如果我们想应用重试,操作必须是幂等的。假设远程服务接收并处理了我们的请求,但在发送响应时出现问题。在这种情况下,当我们重试时,我们不希望服务将请求视为新请求或返回意外错误(想想银行转账)。

重试会增加 API 的响应时间。如果客户端是另一个应用程序,如 cron 作业或守护进程,这可能不是问题。但是,如果是一个人,有时最好做出响应,快速失败并提供反馈,而不是在我们不断重试时让这个人等待。

对于某些关键用例,可靠性可能比响应时间更重要,即使客户是个人,我们也可能需要实现重试。银行转账或旅行社预订航班和旅行酒店的转账就是很好的例子 – 用户期望可靠性,而不是对此类用例的即时响应。我们可以通过立即通知用户我们已接受他们的请求并在完成后通知他们来做出响应。

使用 Resilience4j 重试模块

RetryRegistryRetryConfigRetryresilience4j-retry 中的主要抽象。RetryRegistry 是用于创建和管理 Retry 对象的工厂。RetryConfig 封装了诸如应该尝试重试多少次、尝试之间等待多长时间等配置。每个 Retry 对象都与一个 RetryConfig 相关联。 Retry 提供了辅助方法来为包含远程调用的函数式接口或 lambda 表达式创建装饰器。

让我们看看如何使用 retry 模块中可用的各种功能。假设我们正在为一家航空公司建立一个网站,以允许其客户搜索和预订航班。我们的服务与 FlightSearchService 类封装的远程服务通信。

简单重试

在简单重试中,如果在远程调用期间抛出 RuntimeException,则重试该操作。 我们可以配置尝试次数、尝试之间等待多长时间等:

RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(2, SECONDS))
  .build();

// Registry, Retry creation omitted

FlightSearchService service = new FlightSearchService();
SearchRequest request = new SearchRequest("NYC", "LAX", "07/31/2020");
Supplier<List<Flight>> flightSearchSupplier =
  () -> service.searchFlights(request);

Supplier<List<Flight>> retryingFlightSearch =
  Retry.decorateSupplier(retry, flightSearchSupplier);

System.out.println(retryingFlightSearch.get());

我们创建了一个 RetryConfig,指定我们最多要重试 3 次,并在两次尝试之间等待 2 秒。如果我们改用 RetryConfig.ofDefaults() 方法,则将使用 3 次尝试和 500 毫秒等待持续时间的默认值。

我们将航班搜索调用表示为 lambda 表达式 – List<Flight>SupplierRetry.decorateSupplier() 方法使用重试功能装饰此 Supplier。最后,我们在装饰过的 Supplier 上调用 get() 方法来进行远程调用。

如果我们想创建一个装饰器并在代码库的不同位置重用它,我们将使用 decorateSupplier()。如果我们想创建它并立即执行它,我们可以使用 executeSupplier() 实例方法代替:

List<Flight> flights = retry.executeSupplier(
  () -> service.searchFlights(request));
这是显示第一个请求失败然后第二次尝试成功的示例输出:

Searching for flights; current time = 20:51:34 975
Operation failed
Searching for flights; current time = 20:51:36 985
Flight search successful
[Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'}, ...]

在已检异常上重试

现在,假设我们要重试已检查和未检查的异常。假设我们正在调用
FlightSearchService.searchFlightsThrowingException(),它可以抛出一个已检查的 Exception。由于 Supplier 不能抛出已检查的异常,我们会在这一行得到编译器错误:

Supplier<List<Flight>> flightSearchSupplier =
  () -> service.searchFlightsThrowingException(request);

我们可能会尝试在 lambda 表达式中处理 Exception 并返回 Collections.emptyList(),但这看起来不太好。更重要的是,由于我们自己捕获 Exception,重试不再起作用:

ExceptionSupplier<List<Flight>> flightSearchSupplier = () -> {
    try {      
      return service.searchFlightsThrowingException(request);
    } catch (Exception e) {
      // don't do this, this breaks the retry!
    }
    return Collections.emptyList();
  };

那么当我们想要重试远程调用可能抛出的所有异常时,我们应该怎么做呢?我们可以使用
Retry.decorateCheckedSupplier()(或 executeCheckedSupplier() 实例方法)代替 Retry.decorateSupplier()

CheckedFunction0<List<Flight>> retryingFlightSearch =
  Retry.decorateCheckedSupplier(retry,
    () -> service.searchFlightsThrowingException(request));

try {
  System.out.println(retryingFlightSearch.apply());
} catch (...) {
  // handle exception that can occur after retries are exhausted
}

Retry.decorateCheckedSupplier() 返回一个 CheckedFunction0,它表示一个没有参数的函数。请注意对 CheckedFunction0 对象的 apply() 调用以调用远程操作。

如果我们不想使用 SuppliersRetry 提供了更多的辅助装饰器方法,如 decorateFunction()decorateCheckedFunction()decorateRunnable()decorateCallable() 等,以与其他语言结构一起使用。decorate*decorateChecked* 版本之间的区别在于,decorate* 版本在 RuntimeExceptions 上重试,而 decorateChecked* 版本在 Exception 上重试。

有条件重试

上面的简单重试示例展示了如何在调用远程服务时遇到 RuntimeException 或已检查 Exception 时重试。在实际应用中,我们可能不想对所有异常都重试。 例如,如果我们得到一个
AuthenticationFailedException 重试相同的请求将无济于事。当我们进行 HTTP 调用时,我们可能想要检查 HTTP 响应状态代码或在响应中查找特定的应用程序错误代码来决定是否应该重试。让我们看看如何实现这种有条件的重试。

Predicate-based条件重试

假设航空公司的航班服务定期初始化其数据库中的航班数据。对于给定日期的飞行数据,此内部操作需要几秒钟时间。 如果我们在初始化过程中调用当天的航班搜索,该服务将返回一个特定的错误代码 FS-167。航班搜索文档说这是一个临时错误,可以在几秒钟后重试该操作。

让我们看看如何创建 RetryConfig

RetryConfig config = RetryConfig.<SearchResponse>custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(3, SECONDS))
  .retryOnResult(searchResponse -> searchResponse
    .getErrorCode()
    .equals("FS-167"))
  .build();

我们使用 retryOnResult() 方法并传递执行此检查的 Predicate。这个 Predicate 中的逻辑可以像我们想要的那样复杂——它可以是对一组错误代码的检查,也可以是一些自定义逻辑来决定是否应该重试搜索。

Exception-based条件重试

假设我们有一个通用异常
FlightServiceBaseException,当在与航空公司的航班服务交互期间发生任何意外时会抛出该异常。作为一般策略,我们希望在抛出此异常时重试。但是我们不想重试 SeatsUnavailableException 的一个子类 – 如果航班上没有可用座位,重试将无济于事。我们可以通过像这样创建 RetryConfig 来做到这一点:

RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(3, SECONDS))
  .retryExceptions(FlightServiceBaseException.class)
  .ignoreExceptions(SeatsUnavailableException.class)
  .build();

retryExceptions() 中,我们指定了一个异常列表。ignoreExceptions() 将重试与此列表中的异常匹配或继承的任何异常。我们把我们想忽略而不是重试的那些放入ignoreExceptions()。如果代码在运行时抛出一些其他异常,比如 IOException,它也不会被重试。

假设即使对于给定的异常,我们也不希望在所有情况下都重试。也许我们只想在异常具有特定错误代码或异常消息中的特定文本时重试。在这种情况下,我们可以使用 retryOnException 方法:

Predicate<Throwable> rateLimitPredicate = rle ->
  (rle instanceof  RateLimitExceededException) &&
  "RL-101".equals(((RateLimitExceededException) rle).getErrorCode());

RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.of(1, SECONDS))
  .retryOnException(rateLimitPredicate)
  build();

与 predicate-based (基于谓词)的条件重试一样,谓词内的检查可以根据需要复杂化。

退避策略

到目前为止,我们的示例有固定的重试等待时间。通常我们希望在每次尝试后增加等待时间——这是为了让远程服务有足够的时间在当前过载的情况下进行恢复。我们可以使用 IntervalFunction 来做到这一点。

IntervalFunction 是一个函数式接口——它是一个以尝试次数为参数并以毫秒为单位返回等待时间的 Function

随机间隔

这里我们指定尝试之间的随机等待时间:

RetryConfig config = RetryConfig.custom()
.maxAttempts(4)
.intervalFunction(IntervalFunction.ofRandomized(2000))
.build();

IntervalFunction.ofRandomized() 有一个关联的 randomizationFactor。我们可以将其设置为 ofRandomized() 的第二个参数。如果未设置,则采用默认值 0.5。这个 randomizationFactor 决定了随机值的分布范围。因此,对于上面的默认值 0.5,生成的等待时间将介于 1000 毫秒(2000 – 2000 0.5)和 3000 毫秒(2000 + 2000 0.5)之间。

这种行为的示例输出如下:

Searching for flights; current time = 20:27:08 729
Operation failed
Searching for flights; current time = 20:27:10 643
Operation failed
Searching for flights; current time = 20:27:13 204
Operation failed
Searching for flights; current time = 20:27:15 236
Flight search successful
[Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'},...]

指数间隔

对于指数退避,我们指定两个值 – 初始等待时间和乘数。在这种方法中,由于乘数,等待时间在尝试之间呈指数增长。例如,如果我们指定初始等待时间为 1 秒,乘数为 2,则重试将在 1 秒、2 秒、4 秒、8 秒、16 秒等之后进行。当客户端是后台作业或守护进程时,此方法是推荐的方法。

以下是我们如何为指数退避创建 RetryConfig

RetryConfig config = RetryConfig.custom()
.maxAttempts(6)
.intervalFunction(IntervalFunction.ofExponentialBackoff(1000, 2))
.build();

这种行为的示例输出如下:

Searching for flights; current 
time = 20:37:02 684

Operation failed

Searching for flights; current time = 20:37:03 727

Operation failed

Searching for flights; current time = 20:37:05 731

Operation failed

Searching for flights; current time = 20:37:09 731

Operation failed

Searching for flights; current time = 20:37:17 731

IntervalFunction 还提供了一个 exponentialRandomBackoff() 方法,它结合了上述两种方法。我们还可以提供 IntervalFunction 的自定义实现。

重试异步操作

直到现在我们看到的例子都是同步调用。让我们看看如何重试异步操作。假设我们像这样异步搜索航班:

CompletableFuture.supplyAsync(() -> service.searchFlights(request))
  .thenAccept(System.out::println);

searchFlight() 调用发生在不同的线程上,当它返回时,返回的 List<Flight> 被传递给 thenAccept(),它只是打印它。

我们可以使用 Retry 对象上的 executeCompletionStage() 方法对上述异步操作进行重试。 此方法采用两个参数 – 一个 ScheduledExecutorService 将在其上安排重试,以及一个 Supplier<CompletionStage> 将被装饰。它装饰并执行 CompletionStage,然后返回一个 CompletionStage,我们可以像以前一样调用 thenAccept

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

Supplier<CompletionStage<List<Flight>>> completionStageSupplier =
  () -> CompletableFuture.supplyAsync(() -> service.searchFlights(request));

retry.executeCompletionStage(scheduler, completionStageSupplier)
.thenAccept(System.out::println);

在实际应用程序中,我们将使用共享线程池 (
Executors.newScheduledThreadPool()) 来调度重试,而不是此处显示的单线程调度执行器。

重试事件

在所有这些例子中,装饰器都是一个黑盒子——我们不知道什么时候尝试失败了,框架代码正在尝试重试。假设对于给定的请求,我们想要记录一些详细信息,例如尝试计数或下一次尝试之前的等待时间。 我们可以使用在不同执行点发布的重试事件来做到这一点。Retry 有一个 EventPublisher,它具有 onRetry()onSuccess() 等方法。

我们可以通过实现这些监听器方法来收集和记录详细信息:

Retry.EventPublisher publisher = retry.getEventPublisher();

publisher.onRetry(event -> System.out.println(event.toString()));

publisher.onSuccess(event -> System.out.println(event.toString()));

类似地,RetryRegistry 也有一个 EventPublisher,它在 Retry 对象被添加或从注册表中删除时发布事件。

重试指标

Retry 维护计数器以跟踪操作的次数

  1. 第一次尝试成功
  2. 重试后成功
  3. 没有重试就失败了
  4. 重试后仍失败

每次执行装饰器时,它都会更新这些计数器。

为什么要捕获指标?

捕获并定期分析指标可以让我们深入了解上游服务的行为。它还可以帮助识别瓶颈和其他潜在问题

例如,如果我们发现某个操作通常在第一次尝试时失败,我们可以调查其原因。如果我们发现我们的请求在建立连接时受到限制或超时,则可能表明远程服务需要额外的资源或容量。

如何捕获指标?

Resilience4j 使用 Micrometer 发布指标。Micrometer 为监控系统(如 Prometheus、Azure Monitor、New Relic 等)提供了仪表客户端的外观。因此我们可以将指标发布到这些系统中的任何一个或在它们之间切换,而无需更改我们的代码。

首先,我们像往常一样创建 RetryConfigRetryRegistryRetry。然后,我们创建一个 MeterRegistry 并将 etryRegistry 绑定到它:

MeterRegistry meterRegistry = new SimpleMeterRegistry();

TaggedRetryMetrics.ofRetryRegistry(retryRegistry).bindTo(meterRegistry);

运行几次可重试操作后,我们显示捕获的指标:

Consumer<Meter> meterConsumer = meter -> {
  String desc = meter.getId().getDescription();
  String metricName = meter.getId().getTag("kind");
  Double metricValue = StreamSupport.stream(meter.measure().spliterator(), false)
    .filter(m -> m.getStatistic().name().equals("COUNT"))
    .findFirst()
    .map(m -> m.getValue())
    .orElse(0.0);
  System.out.println(desc + " - " + metricName + ": " + metricValue);
};
meterRegistry.forEachMeter(meterConsumer);

一些示例输出如下:

The number of successful calls without a retry attempt - successful_without_retry: 4.0

The number of failed calls without a retry attempt - failed_without_retry: 0.0

The number of failed calls after a retry attempt - failed_with_retry: 0.0

The number of successful calls after a retry attempt - successful_with_retry: 6.0

当然,在实际应用中,我们会将数据导出到监控系统并在仪表板上查看。

重试时的注意事项和良好实践

服务通常提供具有内置重试机制的客户端库或 SDK。对于云服务尤其如此。 例如,Azure CosmosDB 和 Azure 服务总线为客户端库提供内置重试工具。 它们允许应用程序设置重试策略来控制重试行为。

在这种情况下,最好使用内置的重试而不是我们自己的编码。如果我们确实需要自己编写,我们应该禁用内置的默认重试策略 – 否则,它可能导致嵌套重试,其中应用程序的每次尝试都会导致客户端库的多次尝试

一些云服务记录瞬时错误代码。例如,Azure SQL 提供了它期望数据库客户端重试的错误代码列表。在决定为特定操作添加重试之前,最好检查一下服务提供商是否有这样的列表。

另一个好的做法是将我们在 RetryConfig 中使用的值(例如最大尝试次数、等待时间和可重试错误代码和异常)作为我们服务之外的配置进行维护。如果我们发现新的暂时性错误或者我们需要调整尝试之间的间隔,我们可以在不构建和重新部署服务的情况下进行更改。

通常在重试时,框架代码中的某处可能会发生 Thread.sleep()。对于在重试之间有等待时间的同步重试就是这种情况。如果我们的代码在 Web 应用程序的上下文中运行,则 Thread 很可能是 Web 服务器的请求处理线程。因此,如果我们进行过多的重试,则会降低应用程序的吞吐量

结论

在本文中,我们了解了 Resilience4j 是什么,以及如何使用它的重试模块使我们的应用程序可以在应对临时错误具备弹性。我们研究了配置重试的不同方法,以及在不同方法之间做出决定的一些示例。我们学习了一些在实施重试时要遵循的良好实践,以及收集和分析重试指标的重要性。

您可以使用 GitHub 上的代码尝试一个完整的应用程序来演示这些想法。


本文译自: Implementing Retry with Resilience4j – Reflectoring

搭建 Restful Web 服务

1. 理解 REST

  REST 全称是 Representational State Transfer,中文意思是表征性状态转移。它首次出现在2000年Roy Fielding的博士论文中,Roy Fielding是HTTP规范的主要编写者之一。值得注意的是REST并没有一个明确的标准,而更像是一种设计的风格。如果一个架构符合REST的约束条件和原则,我们就称它为RESTful架构。

  理论上REST架构风格并不是绑定在HTTP上,只不过目前HTTP是唯一与REST相关的实例。

1.1. REST 原则

  • 资源 可通过目录结构样式的 URIs 暴露

  • 表述 可以通过 JSON 或 XML 表达的数据对象或属性来传递

  • 消息 使用统一的 HTTP 方法(例如:GET、POST、PUT 和 DELETE)

  • 无状态 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息

    1.2. HTTP 方法

      使用 HTTP 将 CRUD(create, retrieve, update, delete <创建、获取、更新、删除—增删改查>)操作映射为 HTTP 请求。如果按照HTTP方法的语义来暴露资源,那么接口将会拥有安全性和幂等性的特性,例如GET和HEAD请求都是安全的, 无论请求多少次,都不会改变服务器状态。而GET、HEAD、PUT和DELETE请求都是幂等的,无论对资源操作多少次, 结果总是一样的,后面的请求并不会产生比第一次更多的影响。

    1.2.1. GET

  • 安全且幂等

  • 获取信息

    1.2.2. POST

  • 不安全且不幂等

  • 使用请求中提供的实体执行操作,可用于创建资源或更新资源

    1.2.3. PUT

  • 不安全但幂等

  • 使用请求中提供的实体执行操作,可用于创建资源或更新资源

    1.2.4. DELETE

  • 不安全但幂等

  • 删除资源
      POST和PUT在创建资源的区别在于,所创建的资源的名称(URI)是否由客户端决定。 例如为我的博文增加一个java的分类,生成的路径就是分类名/categories/java,那么就可以采用PUT方法。不过很多人直接把POST、GET、PUT、DELETE直接对应上CRUD,例如在一个典型的rails实现的RESTful应用中就是这么做的。

    1.3. HTTP status codes

      状态码指示 HTTP 请求的结果:

  • 1XX:信息

  • 2XX:成功

  • 3XX:转发

  • 4XX:客户端错误

  • 5XX:服务端错误

    1.4. 媒体类型

      HTTP头中的 Accept 和 Content-Type 可用于描述HTTP请求中发送或请求的内容。如果客户端请求JSON响应,那么可以将 Accept 设为 application/json。相应地,如果发送的内容是XML,那么可以设置 Content-Type 为 application/xml 。

    2. REST API 设计最佳实践

      这里介绍一些设计 REST API 的最佳实践,大家先记住下面这句话:

    URL 是个句子,其中资源是名词、HTTP 方法是动词。

    2.1. 使用名词来表示资源

      下面是一些例子:

  • GET – /users:返回用户列表

  • GET – /users/100:返回一个特定用户

  • POST – /users:创建一个新用户

  • PUT – /users/200:更新一个特定用户

  • DELETE – /users/711:删除一个特定用户
      不要使用动词:

  • /getAllsers

  • /getUserById

  • /createNewUser

  • /updateUser

  • /deleteUser

    2.2 在 HTTP 头中使用适当的序列化格式

      客户端和服务端都需要知道通信所用的格式,这个格式要在 HTTP 头中指定:

  • Content-Type 定义请求格式

  • Accept 定义一个可接受的响应格式列表

    2.3 Get 方法和查询参数不应当改变状态

      使用 PUT, POST 和 DELETE 方法来改变状态,不要使用 GET 方法来改变状态:

  • GET /users/711?activate

  • GET /users/711/activate

    2.4. 使用子资源表示关联

      如果一个资源与另一个资源关联,使用子资源:

  • GET /cars/711/drivers/ 返回711号汽车的驾驶员列表

  • GET /cars/711/drivers/4 返回711号汽车的第4号驾驶员

    2.5. 使用适当的 HTTP 方法 (动词)

      再回顾一下这句话:

    URL 是个句子,其中资源是名词、HTTP 方法是动词。

  • GET:获取在URI资源中指定的表述,响应消息体包含所请求资源的细节。

  • POST:创建一个URI指定的新资源,请求消息体提供新资源的细节。注意,POST也可以触发一些操作,而不一定是要创建新资源。

  • PUT:创建或替代指定URI的资源。请求消息体指定要创建或更新的资源。

  • DELETE:移除指定URI的资源。

    2.6. HTTP 响应状态码

      当客户端通过API向服务端发起一个请求时,客户端应当知道反馈:是否失败、通过或者请求错误。HTTP 状态码是一批标准化代码,在不同的场景下有不同的解释。服务器应当总是返回正确的状态码。
      下面是重要的HTTP代码分类:

  • 2xx (成功分类):这些状态码代码请求动作被接收且被服务器成功处理。

    • 200:Ok 表示 GET、PUT 或 POST 请求的标准状态码。
    • 201:Created(已创建)表示实例已被创建,多用于 POST 方法。
    • 204:No Content(无内容)表示请求已被成功处理但没有返回任何内容。常用于 DELETE 方法返回。
  • 3xx (转发分类)

    • 304:Not Modified(无修改)表示客户端已经缓存此响应,无须再次传输相同内容。
  • 4xx (客户端错误分类):这些状态码代表客户端提交了一个错误请求。

    • 400:Bad Request(错误请求)表示客户端请求没被处理,因为服务端无法理解客户端请求。
    • 401:Unauthorized(无授权)表示客户端无权访问资源,应当加上所需的认证信息后再次请求。
    • 403:Forbidden(禁止访问)表示请求有效且客户端已获授权,但客户端无权访问该资源。
    • 404:Not Found(没发现)表示所请求的资源现在不可用。
    • 410:Gone(移除)表示所请求的资源已被移除。
  • 5xx (服务端错误分类)

    • 500:Internal Server Error(内部服务器错误)表示请求有效,但是服务端发生了异常。
    • 503:Service Unavailable(服务不可用)表示服务器关闭或不可用,通常是指服务器处于维护状态。

      2.7. 名称规约

        你可以遵循任何名称规约,只要保持跨应用一致性即可。如果请求体和响应体是 JSON 类型,那么请遵循驼峰名称规约。

      2.8. 搜索、排序、过滤与分页

        上面一些示例都是在一个数据集上的简单查询,对于复杂的数据,我们需要在 GET 方法 API 上加一些参数来处理。下面是一些示例:

  • 排序:这个例子中,客户想获取排序的公司列表,GET /companies 应当在查询时接受多种排序参数。譬如 GET /companies?sort=rank_asc 将以等级升序的方式对公司进行排序。

  • 过滤:要过滤数据集,我们可以通过查询参数传递不同的选项。比如 GET /companies?category=banking&location=india 将过滤分类为银行且位于印度的公司。

  • 搜索:在公司列表中搜索公司名的 API 端点应当是 GET /companies?search=Digital。

  • 分页:当数据集太大时,我们应当将数据集分割成小的数据块,这样有利于提升服务端性能,也方便客户端处理响应。如 GET /companies?page=23 意味着获取公司列表的第 23 页数据。

    2.9. Restful API 版本

      一般使用不带点的简单数字表示版本,数字前加字母v代表版本号,如下所示:

  • /blog/api/v1

  • http://api.yourservice.com/v1/companies/34/employees

    2.10. 处理 JSON 错误体

      API 错误处理机制是很重要的,而且要好好规划。极力推荐总是在返回字段中包含错误消息。一个 JSON 错误体应当为开发者提供一些有用的信息:错误消息、错误代码以及详细描述。下面是一个较好的示例:

    {
    "code": 1234,
    "message": "Something bad happened :(",
    "description": "More details about the error here"
    }

    2.11. 如何创建 Rest API URL

      推荐使用下面格式的 URL:

  • http(s)://{域名(:端口号)}/{表示REST API的值}/{API版本}/{识别资源的路径}

  • http(s)://{表示REST API的域名(:端口号)}/{API 版本}/{识别资源的路径}
      如下所示:

  • http://example.com/api/v1/members/M000000001

  • http://api.example.com/v1/members/M000000001

    3. 开发基于 Spring Boot 的 Restful Web 服务

      Spring Boot 提供了构建企业应用中 RESTful Web 服务的极佳支持。

    3.1. 引入依赖

      要构建 RESTful Web 服务,我们需要在构建配置文件中加上 Spring Boot Starter Web 依赖。
      对于 Maven 用户,使用以下的代码在 pom.xml 文件中加入依赖:

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

      对于 Gradle 用户,使用以下的代码在 build.gradle 文件中加入依赖:

    compile('org.springframework.boot:spring-boot-starter-web')

    3.2. Rest 相关注解

      在继续构建 RESTful web 服务前,建议你先要熟悉下面的注解:

    Rest Controller

      @RestController 注解用于定义 RESTful web 服务。它提供 JSON、XML 和自定义响应。语法如下所示:

    @RestController
    public class ProductServiceController {
    }

    Request Mapping

      @RequestMapping 注解用于定义请求 URI 以访问 REST 端点。我们可以定义 Request 方法来消费 produce 对象。默认的请求方法是 GET:

    @RequestMapping(value = "/products")
    public ResponseEntity<Object> getProducts() { }
    Request Body
    @RequestBody 注解用于定义请求体内容类型。
    public ResponseEntity<Object> createProduct(@RequestBody Product product) {
    }

    Path Variable

      @PathVariable 注解被用于定义自定义或动态的请求 URI,Path variable 被放在请求 URI 中的大括号内,如下所示:

    public ResponseEntity<Object> updateProduct(@PathVariable("id") String id) {
    }

    Request Parameter

      @RequestParam 注解被用于从请求 URL 中读取请求参数。缺省情况下是必须的,也可以为请求参数设置默认值。如下所示:
    public ResponseEntity getProduct(
    @RequestParam(value = "name", required = false, defaultValue = "honey") String name) {
    }

    3.3. 编写 REST API

    GET API

      下面的示例代码定义了 HTTP GET 请求方法。在这个例子里,我们使用 HashMap 来在存储 Product。注意我们使用了 POJO 类来存储产品。
      在这里,请求 URI 是 /products,它会从 HashMap 仓储中返回产品列表。下面的控制器类文件包含了 GET 方法的 REST 端点:

    
    package com.tutorialspoint.demo.controller;

    import java.util.HashMap;
    import java.util.Map;

    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import com.tutorialspoint.demo.model.Product;

    @RestController
    public class ProductServiceController {
    private static Map<String, Product> productRepo = new HashMap<>();
    static {
    Product honey = new Product();
    honey.setId("1");
    honey.setName("Honey");
    productRepo.put(honey.getId(), honey);

      Product almond = new Product();
      almond.setId("2");
      almond.setName("Almond");
      productRepo.put(almond.getId(), almond);

    }
    @RequestMapping(value = "/products")
    public ResponseEntity<Object> getProduct() {
    return new ResponseEntity<>(productRepo.values(), HttpStatus.OK);
    }
    }

    ### POST API
      HTTP POST 请求用于创建资源。这个方法包含请求体。我们可以通过发送请求参数和路径变量来定义自定义或动态 URL。
      下面的示例代码定义了 HTTP POST 请求方法。在这个例子中,我们使用 HashMap 来存储 Product,这里产品是一个 POJO 类。
      这里,请求 URI 是 /products,在产品被存入 HashMap 仓储后,它会返回字符串。
    ```java
    package com.tutorialspoint.demo.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tutorialspoint.demo.model.Product;
    
    @RestController
    public class ProductServiceController {
       private static Map<String, Product> productRepo = new HashMap<>();
    
       @RequestMapping(value = "/products", method = RequestMethod.POST)
       public ResponseEntity<Object> createProduct(@RequestBody Product product) {
          productRepo.put(product.getId(), product);
          return new ResponseEntity<>("Product is created successfully", HttpStatus.CREATED);
       }
    }

    PUT API

      HTTP PUT 请求用于更新已有的资源。这个方法包含请求体。我们可以通过发送请求参数和路径变量来定义自定义或动态 URL。
      下面的例子展示了如何定义 HTTP PUT 请求方法。在这个例子中,我们使用 HashMap 更新现存的产品。此处,产品是一个 POJO 类。
      这里,请求 URI 是 /products/{id},在产品被存入 HashMap 仓储后,它会返回字符串。注意我们使用路径变量 {id} 定义需要更新的产品 ID:

    package com.tutorialspoint.demo.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    import com.tutorialspoint.demo.model.Product;
    
    @RestController
    public class ProductServiceController {
       private static Map<String, Product> productRepo = new HashMap<>();
    
       @RequestMapping(value = "/products/{id}", method = RequestMethod.PUT)
       public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Product product) {
          productRepo.remove(id);
          product.setId(id);
          productRepo.put(id, product);
          return new ResponseEntity<>("Product is updated successsfully", HttpStatus.OK);
       }   
    }

    DELETE API

      HTTP Delete 请求用于删除存在的资源。这个方法不包含任何请求体。我们可以通过发送请求参数和路径变量来定义自定义或动态 URL。
      下面的例子展示如何定义 HTTP DELETE 请求方法。这个例子中,我们使用 HashMap 来移除现存的产品,用 POJO 来表示。
      请求 URI 是 /products/{id} 在产品被从 HashMap 仓储中删除后,它会返回字符串。 我们使用路径变量 {id} 来定义要被删除的产品 ID。

    package com.tutorialspoint.demo.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.tutorialspoint.demo.model.Product;
    
    @RestController
    public class ProductServiceController {
       private static Map<String, Product> productRepo = new HashMap<>();
    
       @RequestMapping(value = "/products/{id}", method = RequestMethod.DELETE)
       public ResponseEntity<Object> delete(@PathVariable("id") String id) {
          productRepo.remove(id);
          return new ResponseEntity<>("Product is deleted successsfully", HttpStatus.OK);
       }
    }