|
<strong>public</strong> <strong>class</strong> MakerBot {
<strong>private</strong> Client client;
<strong>private</strong> WebTarget target;
@PostConstruct
<strong>private</strong> <strong>void</strong> initClient() {
client = ClientBuilder.newBuilder()
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(4, TimeUnit.SECONDS)
.build();
target = client
.target(<span style=“color:#00bb00”>“http://maker-bot:9080/maker-bot/resources/jobs”</span><span style=“color:black”>);
}
<strong>public</strong> <strong>void</strong> printInstrument(InstrumentType type) {
JsonObject requestBody = createRequestBody(type);
Response response = sendRequest(requestBody);
validateResponse(response);
}
</span><span style=“color:#0000aa”><em>// …</em></span><span style=“color:black”>
}
</span></span>
从JAX-RS 2.1版开始,ClientBuilder通过connectTimeout()和readTimeout()方法支持标准化的超时配置。根据所使用的HTTP实现,不指定超时值可能最终会无限制地阻塞调用。
当然,实际的超时值取决于实际的应用程序和环境设置。
断路器
与电气工程中的断路器类似,软件中的断路器检测故障或响应缓慢,并通过抑制注定要失败的动作来防止进一步损坏。我们可以指定断路器应该根据先前的执行中断某些功能的执行情况。
有多个第三方库可用于实现断路器,包括MicroProfile Fault Tolerance项目,该项目与Java EE非常好地集成,并得到少数应用程序容器供应商的支持。以下声明该类的printInstrument方法MakerBot由具有默认行为的MicroProfile断路器保护:
|
<strong>public</strong> <strong>void</strong> printInstrument(InstrumentType type) {
JsonObject requestBody = createRequestBody(type);
Response response = sendRequest(requestBody);
validateResponse(response);
}
</span>
如果@CircuitBreaker方法执行失败,那么注释将导致方法执行被中断 - 也就是说,如果它在20次调用中超过50%的时间抛出异常 - 默认情况下。电路打开后,执行将默认中断至少5秒。可以使用注释覆盖这些默认值。
可以使用@Fallback注释定义回退行为,注释分别引用回退处理程序类或方法。
重试
重试背后的动机是通过立即重试失败的操作来消除暂时的失败。此重试对调用功能透明地发生。
使用MicroProfile Fault Tolerance实现技术动机重试很简单。@Retry如果发生异常,注释将导致方法调用最多重新执行三次。我们可以使用注释值进一步配置行为,例如延迟时间或异常类型。
与断路器类似,@Fallback如果在最大重试次数后调用仍然失败,也可以定义行为。
隔板
与船上的隔间类似,隔板旨在将软件功能划分为可单独失效的部分,而不会导致整个应用程序无响应。它们可以防止错误进一步级联,同时应用程序的其余部分保持正常运行。
在企业Java中,通过定义多个池(例如数据库连接池或线程池)来应用Bulkhead模式。对于多个线程池,如果另一个线程池当前用尽,我们可以确保应用程序的特定部分不受影响。
但是,企业Java应用程序不应该启动或管理自己的线程; 相反,他们必须使用平台功能来提供托管线程。为此,Java EE附带一个ManagedExecutorService提供容器管理线程的线程,通常基于单个线程池。
由Java EE专家Adam Bien提供的Porcupine库支持进一步定义可以单独配置的容器管理线程池。以下显示了两个专用ExecutorService的检索和创建工具的定义和用法:
|
@Dedicated(<span style=“color:#00bb00”>“instruments-read”</span><span style=“color:black”>)
ExecutorService readExecutor;
@Inject
@Dedicated(</span><span style=“color:#00bb00”>“instruments-write”</span><span style=“color:black”>)
ExecutorService writeExecutor;
</span><span style=“color:#0000aa”><em>// usage within method body …</em></span><span style=“color:black”>
CompletableFuture.supplyAsync(() -> instrumentCraftShop.getInstruments(), readExecutor);
</span><span style=“color:#0000aa”><em>// usage within method body …</em></span><span style=“color:black”>
CompletableFuture.runAsync(() -> instrumentCraftShop.craftInstrument(instrument), writeExecutor)
</span></span>
通常,这些执行程序服务可以在我们的整个应用程序中使用。但是,在使用HTTP资源时需要考虑另一种情况。
应用程序服务器通常使用单个线程池来处理传入请求的HTTP请求线程。当然,单个请求线程池使我们很难在我们的应用程序中构建多个隔板。
因此,我们可以使用异步JAX-RS资源来立即管理对专用执行程序服务的传入请求的处理:
|
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
<strong>public</strong> <strong>class</strong> InstrumentsResource {
@Inject
InstrumentCraftShop instrumentCraftShop;
@Inject
@Dedicated(</span><span style=“color:#00bb00”>“instruments-read”</span><span style=“color:black”>)
ExecutorService readExecutor;
@Inject
@Dedicated(</span><span style=“color:#00bb00”>“instruments-write”</span><span style=“color:black”>)
ExecutorService writeExecutor;
@GET
<strong>public</strong> CompletionStage<List<Instrument>> getInstruments() {
<strong>return</strong> CompletableFuture
.supplyAsync(() -> instrumentCraftShop.getInstruments(), readExecutor);
}
@POST
<strong>public</strong> CompletionStage<Response> createInstrument(@Valid @NotNull Instrument instrument) {
<strong>return</strong> CompletableFuture.runAsync(
() -> instrumentCraftShop.craftInstrument(instrument), writeExecutor)
.thenApply(c -> Response.noContent().build())
.exceptionally(e -> Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.header(</span><span style=“color:#00bb00”>“X-Error”</span><span style=“color:black”>, e.getMessage())
.build());
}
}
</span></span>
检索和创建工具的传入请求将传递到单独的线程池。从JAX-RS 2.1开始,返回一个CompletionStage或兼容的类型就足以将JAX-RS资源声明为异步。异步处理完成后,请求将被暂停并恢复。
如果分别用于检索或创建工具的这两个功能中的一个将被重载并且池中的线程用完,则另一个将不受此影响。共享请求线程池不太可能用完线程,因为请求的处理会立即传递给其他受管线程。
我们使用Porcupine库而不是MicroProfile Fault Tolerance的Bulkhead功能的原因是:前者使我们能够直接访问和控制我们与异步JAX-RS资源连接的托管执行程序服务。
背压
负载较重的应用程序可以通过向客户通知其当前状态来应用背压。这可以通过多种方式实现,例如,通过向响应添加元数据或者通过返回失败响应来更加彻底地添加元数据。
如果我们认为应用程序保持响应更为重要,尤其是它能够在其服务级别协议(SLA)内响应而不是延迟响应,那么我们将希望实现背压。关于满足整个系统的SLA,立即响应错误以使客户端可以调用不同的应用程序或实例,而不是消耗所有SLA时间并且仍然无法正常运行可能更有帮助处理请求。
为了指示我们的执行程序服务在所有线程都忙的情况下立即拒绝超过执行程序等待队列的调用,我们需要进一步配置行为。ExecutorConfigurator是Porcupine使用的托管bean,我们可以使用CDI专门使用它: