我第一次写这篇文章是为了2020年的Java Advent calendar。然后我把它复制到我自己的博客:


圣诞节就要到了!像每年一样,圣诞老人需要安排它的礼物交付。你能想象吗?数以百万计的(好)孩子将在12月24日晚上收到一份礼物。为了达到这个目的,圣诞老人需要一点技术上的帮助来安排交付。使用MicroProfile和Quarkus的微服务架构怎么样?
弹性的微服务架构
圣诞老人过去一直在使用几种技术架构,但现在,他想转向微服务!他已经找到了外部合作伙伴,与之合作。他已经找到了外部合作伙伴,所以他希望我们开发一个有弹性的架构来与这些第三方的微服务进行交互。
因此,我们需要开发圣诞老人自己的微服务(称为圣诞老人),有自己的数据库,以安排他即将到来的交货。通过这个微服务,圣诞老人可以找到他过去在每个国家的送货情况,但更重要的是,他可以为2020年的圣诞节创建新的时间表。为此,圣诞老人访问了由外部合作伙伴开发和维护的两个外部微服务(JSON over HTTP)。
- 孩子:第三方微服务,为圣诞老人提供每个国家应得礼物的好孩子名单(顽皮的孩子今年什么都得不到)。
- 口袋妖怪:第三方微服务返回口袋妖怪(今年圣诞老人将只提供口袋妖怪):


12月24日,圣诞老人将只有几个小时的时间将口袋妖怪送到全球所有的好孩子手中。这意味着,圣诞老人微服务将调用儿童微服务,以获得所有好孩子的位置,每个国家,然后,对于每个孩子,它调用口袋妖怪微服务来获得礼物。因此,该系统不能失败我们需要建立一个有弹性的架构。
如果圣诞老人的微服务不能访问两个外部微服务(例如,由于天气状况导致的网络问题),我们需要找到一个备份计划。作为一个备份,如果圣诞老人不能访问Kid微服务,他可以根据前一年的时间表(存储在本地数据库中)创建新的时间表。如果Pokemon微服务没有回应,那么,圣诞老人会送一些棒棒糖来代替(他有大量的棒棒糖)。
为此,我们可以使用MicroProfile与Quarkus。
MicroProfile和Quarkus
Eclipse MicroProfile解决了企业Java微服务的需求。它是一套处理微服务设计模式的规范。MicroProfile API通过采用Jakarta EE标准的一个子集,并对其进行扩展以解决常见的微服务模式,为开发基于微服务的应用程序建立了一个最佳基础。Eclipse MicroProfile是在Eclipse基金会下指定的,并由以下机构实施 SmallRye.
如果你还没有听说过 Quarkus,你肯定应该看看它。Quarkus是一个为OpenJDK HotSpot和GraalVM定制的Kubernetes Native Java堆栈,由最优秀的Java库和标准精心打造。实际上,Quarkus是一个用于编写Java应用的开源栈,特别是后端应用。所以Quarkus并不局限于微服务,尽管它非常适用于微服务。所有这些都没有通过提出新的编程模型来重新发明车轮,Quarkus利用了你已经知道的标准库(如CDI、JPA、Bean Validation、JAX-RS等)以及许多流行框架(如Vert.x、Apache Camel等)的经验。
为了建立一个有弹性的Santa系统,我们将使用Quarkus和两个MicroProfile规范*:Eclipse MicroProfile REST Client和Eclipse MicroProfile Fault Tolerance*。
让我们用Quarkus和JAX-RS来开发Santa REST端点。
Santa REST端点
为了与Santa微服务进行交互,我们将开发一个名为SantaResource 的JAX-RS端点:


如上图所示,SantaResource 有两个方法。
createASchedule():用HTTP POST调用,该方法为一个国家创建时间表。getASchedule():以HTTP GET方式调用,该方法返回一个给定国家和给定年份的时间表。
createASchedule() 方法将业务逻辑和数据库访问委托给一个SantaService 。这个服务是调用远程第三方微服务Kid和Pokemon的服务。它也通过Schedule 实体访问数据库。为一个特定的年份(在我们的例子中是2020年)和国家制定了一个时间表。它有一组Delivery 实体:一个交付是指圣诞老人向一个孩子交付一份礼物:
[sourcecode language="java"]
@Path("/api/santa")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.TEXT_PLAIN)
public class SantaResource {
@Inject
SantaService service;
@POST
@Transactional
public Schedule createASchedule(String country) {
Schedule schedule = service.getAllGoodChildren(country);
schedule = service.getEachChildAPresent(schedule);
schedule.persist();
return schedule;
}
@GET
public Optional<Schedule> getASchedule(@QueryParam("country") String country,
@DefaultValue("2020") @QueryParam("year") int year) {
return Schedule.findByYearAndCountry(year, country);
}
}
[/sourcecode]
圣诞老人调用这个端点的方式如下:
[sourcecode language="term"]
# Creates a new schedule for Angola
curl -X POST -H "Content-Type: text/plain" -d "Angola" http://localhost:8701/api/santa
# Gets the 2019 schedule for Venezuela
curl "http://localhost:8701/api/santa?country=Venezuela&year=2019"
[/sourcecode]
现在让我们使用Eclipse MicroProfile REST Client,这样SantaService 可以访问远程微服务。
MicroProfile Rest Client
Eclipse MicroProfile REST Client提供了一种使用代理和注解的类型安全方法,用于通过HTTP调用RESTful服务。Eclipse MicroProfile REST客户端建立在JAX-RS APIs之上,以实现一致性和易用性。如下图所示,在Eclipse MicroProfile REST Client中,无需使用低级别的HTTP APIs来调用远程微服务,而只需使用一个Java接口即可。


正如上面的SantaResource ,方法createASchedule() 调用getAllGoodChildren() 的SantaService 。getAllGoodChildren() 使用ChildProxy 与远程Kid微服务进行通信。为此,我们使用标准的CDI@Inject 注解与MicroProfile@RestClient 注解相结合,注入ChildProxy 接口:
[sourcecode language="java"]
@ApplicationScoped
public class SantaService {
@Inject
@RestClient
ChildProxy childProxy;
public Schedule getAllGoodChildren(String country) {
Schedule schedule = new Schedule(2020, country);
List<Child> allChildrenPerCountry = childProxy.getAllGoodChildren(country);
for (Child child : allChildrenPerCountry) {
schedule.addDelivery(child);
}
return schedule;
}
}
[/sourcecode]
@RegisterRestClient允许Quarkus知道这个接口是作为REST客户端可用于CDI注入的。@Path和 是标准的 JAX-RS 注解,用于定义如何访问远程服务。@GET@Produces定义了预期的内容类型。
[sourcecode language="java"]
@Path("/api/kids")
@RegisterRestClient(configKey = "child-proxy")
public interface ChildProxy {
@GET
@Produces(MediaType.APPLICATION_JSON)
List<Child> getAllGoodChildren(@QueryParam("country") String country);
}
[/sourcecode]
ChildProxy 返回一个Child 对象的列表。一个Child 有圣诞老人送礼物所需的所有信息(名字、地址和一个表示房子是否有烟囱的布尔值:
[sourcecode language="java"]
public class Child {
public String name;
public String address;
public boolean chimney;
}
[/sourcecode]
只缺少一个信息:即远程Kid微服务的位置。
这只是在Quarkus的application.properties 文件中配置的问题。
为此,我们使用child-proxy 属性键(见@RegisterRestClient 注释)并设置一个URL值(这里是http://localhost:8702):
[sourcecode language="term"]
child-proxy/mp-rest/url=http://localhost:8702
present-proxy/mp-rest/url=http://localhost:8703
[/sourcecode]
在这个配置文件中,你也注意到了远程口袋妖怪微服务的URL。
让我们做同样的事情,使用Eclipse MicroProfile REST Client通过代理来调用它。
一旦圣诞老人得到了所有好孩子的名单,他就会调用另一个微服务来为每个孩子得到一个玩具。如下图所示,SantaService 使用另一个代理,PresentProxy 来调用远程Pokemon微服务:
[sourcecode language="java"]
@Inject
@RestClient
PresentProxy presentProxy;
public Schedule getEachChildAPresent(Schedule schedule) {
for (Delivery delivery : schedule.deliveries) {
delivery.presentName = presentProxy.getAPresent().name;
}
return schedule;
}
[/sourcecode]
PresentProxy 是一个使用一些JAX-RS和Eclipse MicroProfile REST客户端注释的接口:
[sourcecode language="java"]
@Path("/api/pokemons/random")
@RegisterRestClient(configKey = "present-proxy")
public interface PresentProxy {
@GET
@Produces(MediaType.APPLICATION_JSON)
Present getAPresent();
}
[/sourcecode]
一切看起来都很好:Santa微服务现在可以调用Kid和Pokemon微服务了!但圣诞老人非常小心。他以前也见过其他的架构,也见过在他需要的时候失败。如果通信中断怎么办?
MicroProfile容错
如果其中一个远程微服务没有做出预期的反应,例如由于网络通信的脆弱性,我们必须对这种特殊情况进行补偿。 Eclipse MicroProfile容错允许我们建立起微服务架构,使其具有弹性和容错性。这意味着我们不仅要能检测到任何问题,还要能自动处理它。
让我们提供一个后备方案,以便在故障时获得所有好孩子的列表。如下图所示,如果与Kid微服务的通信失败,我们可以得到前一年的时间表。当然,这不是100%的准确(有些孩子今年可能很调皮),但至少圣诞老人可以得到去年圣诞节的孩子的名字和位置。至于口袋妖怪的微服务,如果我们无法联系到它,我们就会退回到提供棒棒糖而不是口袋妖怪。


为此,我们在SantaService 中添加了一个回退方法,名为getLastYearSchedule() 。然后,我们将Eclipse MicroProfile Fault Tolerance @Fallback 注解添加到getAllGoodChildren() 方法中,指向新创建的getLastYearSchedule() 方法。我们还向该方法添加@Retry 注释。我们配置@Retry ,使其尝试5次调用Kid微服务。在每次重试之前,它都会等待2秒。如果不能进行通信,那么,它将退回到getLastYearSchedule() 。这两个,getLastYearSchedule() 方法必须具有与getAllGoodChildren() 相同的方法签名(在我们的例子中,它接受一个国家并返回一个Schedule 对象)。
[sourcecode language="java"]
@Inject
@RestClient
ChildProxy childProxy;
@Retry(maxRetries = 5, delay = 2, delayUnit = ChronoUnit.SECONDS)
@Fallback(fallbackMethod = "getLastYearSchedule")
public Schedule getAllGoodChildren(String country) {
Schedule schedule = new Schedule(2020, country);
List<Child> allChildrenPerCountry = childProxy.getAllGoodChildren(country);
for (Child child : allChildrenPerCountry) {
schedule.addDelivery(child);
}
return schedule;
}
public Schedule getLastYearSchedule(String country) {
Schedule schedule = Schedule.findByYearAndCountry(2019, country).get();
return deepCopy(schedule);
}
[/sourcecode]
我们还在getEachChildAPresent() 方法上使用了@Fallback 注释:如果我们无法联系到Pokemon微服务,那么,我们将退回到提供棒棒糖作为礼物(总比没有好):
[sourcecode language="java"]
@Inject
@RestClient
PresentProxy presentProxy;
@Fallback(fallbackMethod = "getEachChildSomeLollies")
public Schedule getEachChildAPresent(Schedule schedule) {
for (Delivery delivery : schedule.deliveries) {
delivery.presentName = presentProxy.getAPresent().name;
}
return schedule;
}
public Schedule getEachChildSomeLollies(Schedule schedule) {
for (Delivery delivery : schedule.deliveries) {
delivery.presentName = "Santa Lollies";
}
return schedule;
}
[/sourcecode]
结论
现在,圣诞老人放心了:在12月24日晚上,他将飞越每个国家,并能够将口袋妖怪或棒棒糖送到所有的孩子手中,无论发生什么。
微服务之间的通信是非常具有挑战性的。在分布式架构中,你依赖于一个不可靠的网络:网络可能变慢,可能被切断,一个微服务可能挂掉,并对其他微服务产生多米诺效应,等等。这就是为什么Eclipse MicroProfile为我们带来了一些规范,以缓解微服务通信和处理通信故障:


Eclipse MicroProfile REST Client是一个非常优雅的API,你通过使用一个Java接口来使用类型安全的方法,仅此而已。所有下面的HTTP管道都被处理掉了。Eclipse MicroProfile REST Client将处理所有的网络和编排,使我们的代码中没有这些技术细节。多亏了Eclipse MicroProfile Fault Tolerance,你可以只用几个注解就开发出一个回退机制。
参考资料
如果你想尝试一下这段代码,请从GitHub上下载,构建它,运行它,并确保打破微服务之间的通信,看看回退的效果。