大家好,我是飘渺。在今天的DDD与微服务系列文章中,让我们探讨如何在DDD的分层架构中调用第三方服务以及在微服务中使用OpenFeign的最佳实践。
1. DDD中的防腐层
在应用服务中,经常需要调用外部服务接口来实现某些业务功能,这就在代码层面引入了对外部系统的依赖。例如,下面这段转账的代码逻辑需要调用外部接口服务RemoteService来获取汇率。
这里可以看到,TransferService强烈依赖于RemoteService和ExchangeRateRemote对象。如果外部服务的方法或ExchangeRateRemote字段发生变化,都会影响到ApplicationService的代码。当有多个服务依赖此外部接口时,迁移和改造的成本将会巨大。同时,外部依赖的兜底、限流和熔断策略也会受到影响。
在复杂系统中,我们应该尽量避免自己的代码因为外部系统的变化而修改。那么如何实现对外部系统的隔离呢?答案就是引入防腐层(Anti-Corruption Layer,简称ACL)。
1.1 什么是防腐层
在许多情况下,我们的系统需要依赖其他系统,但被依赖的系统可能具有不合理的数据结构、API、协议或技术实现。如果我们强烈依赖外部系统,就会导致我们的系统受到“腐蚀”。在这种情况下,通过引入防腐层,可以有效地隔离外部依赖和内部逻辑,无论外部如何变化,内部代码尽可能保持不变。
图片
防腐层不仅仅是一层简单的调用封装,在实际开发中,ACL可以提供更多强大的功能:
- 适配器: 很多时候外部依赖的数据、接口和协议并不符合内部规范,通过适配器模式,可以将数据转化逻辑封装到ACL内部,降低对业务代码的侵入。
- 缓存: 对于频繁调用且数据变更不频繁的外部依赖,通过在ACL里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的,通过将缓存逻辑嵌入ACL,能够降低业务代码的复杂度。
- 兜底: 如果外部依赖的稳定性较差,提高系统稳定性的策略之一是通过ACL充当兜底,例如在外部依赖出问题时,返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑通常复杂,如果散布在核心业务代码中,会难以维护。通过集中在ACL中,更容易进行测试和修改。
- 易于测试: ACL的接口类能够很容易的实现Mock或Stub,以便于单元测试。
- 功能开关: 有时候,我们希望在某些场景下启用或禁用某个接口的功能,或者让某个接口返回特定值。我们可以在ACL中配置功能开关,而不会影响真实的业务代码。
1.2 如何实现防腐层
实现ACL防腐层的步骤如下:
- 对于依赖的外部对象,我们提取所需的字段,并创建一个内部所需的DTO类。
- 构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类。Facade可以参考Repository的实现模式,将接口定义在领域层,而将实现放在基础设施层。
- 在ApplicationService中依赖内部的Facade对象。
具体实现如下:
通过ACL改造后,我们的ApplicationService代码如下:
这样,经过ACL改造后,ApplicationService的代码已不再直接依赖外部的类和方法,而是依赖我们自己内部定义的值类和接口。如果未来外部服务发生任何变化,只需修改Facade类和数据转换逻辑,而不需要修改ApplicationService的逻辑。
1.3 小结
在没有防腐层ACL的情况下,系统需要直接依赖外部对象和外部调用接口,调用逻辑如下:
图片
而有了防腐层ACL后,系统只需要依赖内部的值类和接口,调用逻辑如下:
图片
2. 微服务中的远程调用
在构建微服务时,我们经常需要跨服务调用,例如在DailyMart系统中,购物车服务需要调用商品服务以获取商品详细信息。理论上,我们可以遵循上述ACL的实现逻辑,在购物车模块创建Facade接口和内部转换类。然而,在实际开发中,由于是内部系统,差异性不太明显,通常可以直接使用OpenFeign进行远程调用,忽略Facade定义和内部类转换的过程。
以下是在微服务中使用OpenFeign实现跨服务调用的过程:
- 首先,在购物车模块的基础设施层创建一个接口,并使用@FeignClient注解进行标注。
需要注意的是,我们在商品服务中对外提供的商品详情接口定义返回的是ProductRespDTO对象,但通过OpenFeign调用时返回的是Result对象。
这是因为在前文中,我们定义了一个全局的包装类GlobalResponseBodyAdvice,会自动给所有接口封装返回对象Result。因此,在定义Feign接口时,也需要使用Result对象来接收。如果对此逻辑不太清晰,建议参考第七章的内容。
- 在启动类上添加@EnableFeignClient注解
- 在应用服务中注入Feign接口并使用
如上所示,我们可以看到,每次调用Feign接口都需要解析Result对象以获取真正的业务对象。这种代码看起来有些冗余,是否有办法去除呢?
2.1 自定义Feign的解码器
这时,我们可以通过重写Feign的解码器来实现,在解码器中完成封装对象的拆解。
同时,创建一个配置类,替换原生的解码器。
这样,在定义或调用OpenFeign接口时,直接使用原生对象ProductRespDTO即可。
2.2 上游异常统一处理
在使用OpenFeign进行远程调用时,如果HTTP状态码为非200,OpenFeign会触发异常解析并进入默认的异常解码器feign.codec.ErrorDecoder,将业务异常包装成FeignException。此时,如果不做任何处理,调用时可以返回的消息会变成FeignException的消息体,如下所示:
图片
显然,这个包装后的异常我们不需要,应该直接将捕获到的生产者的业务异常抛给前端。那么,如何解决这个问题呢?
可以通过重写OpenFeign的默认异常解码器来实现,代码如下:
此异常解码器直接将异常转化为自定义的RemoteException,表示远程调用异常。
当然,还需要在配置类中注入此异常解码器。
2.3 Feign全局异常处理
在2.2小节中,我们抛出了自定义的业务异常,然而OpenFeign处理响应时会捕获到业务异常并将其转换成DecodeException。
图片
由于DailyMart中的全局异常处理器没有单独处理DecodeException,它会被兜底异常处理器拦截,并返回类似“系统异常,请联系管理员”的错误提示。
因此,要完全使用上游系统的业务异常,还需要定义一个单独的异常处理器来处理DecodeException。这个处理器可以与全局异常处理器分开,代码如下:
如此一来,框架会自动将业务异常传递给调用服务,业务中也无需关心全局包装的拆解问题,这就是OpenFeign远程调用的最佳实践。当然,在DailyMart中可能有许多服务都需要远程调用,我们可以将上述内容构建成一个通用的Starter模块,以便其他业务模块共享。
图片
小结
本文深入研究了领域驱动设计(DDD)和微服务架构中的两个关键概念:防腐层(ACL)和远程调用的最佳实践。在DDD中,我们学习了如何使用ACL来隔离外部依赖,降低系统耦合度。在微服务架构中,我们探讨了如何通过OpenFeign来实现跨服务调用,并解决了全局包装和异常处理的问题,希望本文的内容对您在软件开发项目中有所帮助。