微服務(wù)中,OpenFeign應(yīng)該這樣用才好!
大家好,我是飄渺。在今天的DDD與微服務(wù)系列文章中,讓我們探討如何在DDD的分層架構(gòu)中調(diào)用第三方服務(wù)以及在微服務(wù)中使用OpenFeign的最佳實(shí)踐。
1. DDD中的防腐層
在應(yīng)用服務(wù)中,經(jīng)常需要調(diào)用外部服務(wù)接口來(lái)實(shí)現(xiàn)某些業(yè)務(wù)功能,這就在代碼層面引入了對(duì)外部系統(tǒng)的依賴(lài)。例如,下面這段轉(zhuǎn)賬的代碼邏輯需要調(diào)用外部接口服務(wù)RemoteService來(lái)獲取匯率。
public class TransferServiceImpl implements TransferService{
private RemoteService remoteService;
@Override
public void transfer(Long sourceUserId, String targetUserId, BigDecimal targetAmount){
//...
ExchangeRateRemote exchangeRate = remoteService.getExchangeRate(
sourceAccount.getCurrency(), targetCurrency);
BigDecimal rate = exchangeRate.getRate();
}
//...
}
這里可以看到,TransferService強(qiáng)烈依賴(lài)于RemoteService和ExchangeRateRemote對(duì)象。如果外部服務(wù)的方法或ExchangeRateRemote字段發(fā)生變化,都會(huì)影響到ApplicationService的代碼。當(dāng)有多個(gè)服務(wù)依賴(lài)此外部接口時(shí),遷移和改造的成本將會(huì)巨大。同時(shí),外部依賴(lài)的兜底、限流和熔斷策略也會(huì)受到影響。
在復(fù)雜系統(tǒng)中,我們應(yīng)該盡量避免自己的代碼因?yàn)橥獠肯到y(tǒng)的變化而修改。那么如何實(shí)現(xiàn)對(duì)外部系統(tǒng)的隔離呢?答案就是引入防腐層(Anti-Corruption Layer,簡(jiǎn)稱(chēng)ACL)。
1.1 什么是防腐層
在許多情況下,我們的系統(tǒng)需要依賴(lài)其他系統(tǒng),但被依賴(lài)的系統(tǒng)可能具有不合理的數(shù)據(jù)結(jié)構(gòu)、API、協(xié)議或技術(shù)實(shí)現(xiàn)。如果我們強(qiáng)烈依賴(lài)外部系統(tǒng),就會(huì)導(dǎo)致我們的系統(tǒng)受到“腐蝕”。在這種情況下,通過(guò)引入防腐層,可以有效地隔離外部依賴(lài)和內(nèi)部邏輯,無(wú)論外部如何變化,內(nèi)部代碼盡可能保持不變。
圖片
防腐層不僅僅是一層簡(jiǎn)單的調(diào)用封裝,在實(shí)際開(kāi)發(fā)中,ACL可以提供更多強(qiáng)大的功能:
- 適配器: 很多時(shí)候外部依賴(lài)的數(shù)據(jù)、接口和協(xié)議并不符合內(nèi)部規(guī)范,通過(guò)適配器模式,可以將數(shù)據(jù)轉(zhuǎn)化邏輯封裝到ACL內(nèi)部,降低對(duì)業(yè)務(wù)代碼的侵入。
- 緩存: 對(duì)于頻繁調(diào)用且數(shù)據(jù)變更不頻繁的外部依賴(lài),通過(guò)在ACL里嵌入緩存邏輯,能夠有效的降低對(duì)于外部依賴(lài)的請(qǐng)求壓力。同時(shí),很多時(shí)候緩存邏輯是寫(xiě)在業(yè)務(wù)代碼里的,通過(guò)將緩存邏輯嵌入ACL,能夠降低業(yè)務(wù)代碼的復(fù)雜度。
- 兜底: 如果外部依賴(lài)的穩(wěn)定性較差,提高系統(tǒng)穩(wěn)定性的策略之一是通過(guò)ACL充當(dāng)兜底,例如在外部依賴(lài)出問(wèn)題時(shí),返回最近一次成功的緩存或業(yè)務(wù)兜底數(shù)據(jù)。這種兜底邏輯通常復(fù)雜,如果散布在核心業(yè)務(wù)代碼中,會(huì)難以維護(hù)。通過(guò)集中在ACL中,更容易進(jìn)行測(cè)試和修改。
- 易于測(cè)試: ACL的接口類(lèi)能夠很容易的實(shí)現(xiàn)Mock或Stub,以便于單元測(cè)試。
- 功能開(kāi)關(guān): 有時(shí)候,我們希望在某些場(chǎng)景下啟用或禁用某個(gè)接口的功能,或者讓某個(gè)接口返回特定值。我們可以在ACL中配置功能開(kāi)關(guān),而不會(huì)影響真實(shí)的業(yè)務(wù)代碼。
1.2 如何實(shí)現(xiàn)防腐層
實(shí)現(xiàn)ACL防腐層的步驟如下:
- 對(duì)于依賴(lài)的外部對(duì)象,我們提取所需的字段,并創(chuàng)建一個(gè)內(nèi)部所需的DTO類(lèi)。
- 構(gòu)建一個(gè)新的Facade,在Facade中封裝調(diào)用鏈路,將外部類(lèi)轉(zhuǎn)化為內(nèi)部類(lèi)。Facade可以參考Repository的實(shí)現(xiàn)模式,將接口定義在領(lǐng)域?qū)?,而將?shí)現(xiàn)放在基礎(chǔ)設(shè)施層。
- 在ApplicationService中依賴(lài)內(nèi)部的Facade對(duì)象。
具體實(shí)現(xiàn)如下:
// 自定義的內(nèi)部值類(lèi)
@Data
public class ExchangeRateDTO {
...
}
// 稅率Facade接口
public interface ExchangeRateFacade {
ExchangeRateDTO getExchangeRate(String sourceCurrency, String targetCurrency);
}
// 稅率facade實(shí)現(xiàn)
@Service
public class ExchangeRateFacadeImpl implements ExchangeRateFacade {
@Resource
private RemoteService remoteService;
@Override
public ExchangeRateDTO getExchangeRate(String sourceCurrency, String targetCurrency) {
ExchangeRateRemote exchangeRemote = remoteService.getExchangeRate(sourceCurrency, targetCurrency);
if (exchangeRemote != null) {
ExchangeRateDTO dto = new ExchangeRateDTO();
dto.setXXX(exchangeRemote.getXXX());
return dto;
}
return null;
}
}
通過(guò)ACL改造后,我們的ApplicationService代碼如下:
public class TransferServiceImpl implements TransferService{
private ExchangeRateFacade exchangeRateFacade;
@Override
public void transfer(Long sourceUserId, String targetUserId, BigDecimal targetAmount){
...
ExchangeRateDTO exchangeRate = exchangeRateFacade.getExchangeRate(
sourceAccount.getCurrency(), targetCurrency);
BigDecimal rate = exchangeRate.getRate();
}
...
}
這樣,經(jīng)過(guò)ACL改造后,ApplicationService的代碼已不再直接依賴(lài)外部的類(lèi)和方法,而是依賴(lài)我們自己內(nèi)部定義的值類(lèi)和接口。如果未來(lái)外部服務(wù)發(fā)生任何變化,只需修改Facade類(lèi)和數(shù)據(jù)轉(zhuǎn)換邏輯,而不需要修改ApplicationService的邏輯。
1.3 小結(jié)
在沒(méi)有防腐層ACL的情況下,系統(tǒng)需要直接依賴(lài)外部對(duì)象和外部調(diào)用接口,調(diào)用邏輯如下:
圖片
而有了防腐層ACL后,系統(tǒng)只需要依賴(lài)內(nèi)部的值類(lèi)和接口,調(diào)用邏輯如下:
圖片
2. 微服務(wù)中的遠(yuǎn)程調(diào)用
在構(gòu)建微服務(wù)時(shí),我們經(jīng)常需要跨服務(wù)調(diào)用,例如在DailyMart系統(tǒng)中,購(gòu)物車(chē)服務(wù)需要調(diào)用商品服務(wù)以獲取商品詳細(xì)信息。理論上,我們可以遵循上述ACL的實(shí)現(xiàn)邏輯,在購(gòu)物車(chē)模塊創(chuàng)建Facade接口和內(nèi)部轉(zhuǎn)換類(lèi)。然而,在實(shí)際開(kāi)發(fā)中,由于是內(nèi)部系統(tǒng),差異性不太明顯,通常可以直接使用OpenFeign進(jìn)行遠(yuǎn)程調(diào)用,忽略Facade定義和內(nèi)部類(lèi)轉(zhuǎn)換的過(guò)程。
以下是在微服務(wù)中使用OpenFeign實(shí)現(xiàn)跨服務(wù)調(diào)用的過(guò)程:
- 首先,在購(gòu)物車(chē)模塊的基礎(chǔ)設(shè)施層創(chuàng)建一個(gè)接口,并使用@FeignClient注解進(jìn)行標(biāo)注。
@FeignClient("product-service")
public interface ProductRemoteFacade {
@GetMapping("/api/product/spu/{spuId}")
Result<ProductRespDTO> getProductBySpuId(@PathVariable("spuId") Long spuId);
}
需要注意的是,我們?cè)谏唐贩?wù)中對(duì)外提供的商品詳情接口定義返回的是ProductRespDTO對(duì)象,但通過(guò)OpenFeign調(diào)用時(shí)返回的是Result對(duì)象。
@Operation(summary = "查詢(xún)商品詳情")
@Parameter(name = "spuId", description = "商品spuId")
@GetMapping("/api/product/spu/{spuId}")
public ProductRespDTO getProductBySpuId(@PathVariable("spuId") Long spuId) {
return productRemoteFacade.getProductBySpuId(spuId);
}
這是因?yàn)樵谇拔闹校覀兌x了一個(gè)全局的包裝類(lèi)GlobalResponseBodyAdvice,會(huì)自動(dòng)給所有接口封裝返回對(duì)象Result。因此,在定義Feign接口時(shí),也需要使用Result對(duì)象來(lái)接收。如果對(duì)此邏輯不太清晰,建議參考第七章的內(nèi)容。
- 在啟動(dòng)類(lèi)上添加@EnableFeignClient注解
@SpringBootApplication
@EnableFeignClients("com.jianzh5.dailymart.module.cart.infrastructure.acl")
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}
- 在應(yīng)用服務(wù)中注入Feign接口并使用
@Override
public void getShoppingCartDetail(Long cartId) {
ShoppingCart shoppingCart = shoppingCartRepository.find(new CartId(cartId));
Result<ProductRespDTO> productRespResult = productRemoteFacade.getProductBySpuId(1L);
// 從Result對(duì)象中獲取真實(shí)的業(yè)務(wù)對(duì)象
if(productRespResult.getCode().equals("OK")){
ProductRespDTO data = productRespResult.getData();
}
}
如上所示,我們可以看到,每次調(diào)用Feign接口都需要解析Result對(duì)象以獲取真正的業(yè)務(wù)對(duì)象。這種代碼看起來(lái)有些冗余,是否有辦法去除呢?
2.1 自定義Feign的解碼器
這時(shí),我們可以通過(guò)重寫(xiě)Feign的解碼器來(lái)實(shí)現(xiàn),在解碼器中完成封裝對(duì)象的拆解。
@RequiredArgsConstructor
public class DailyMartResponseDecoder implements Decoder {
private final ObjectMapper objectMapper;
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
Result<?> result = objectMapper.readValue(response.body().asInputStream(), objectMapper.constructType(Result.class));
if(result.getCode().equals("OK")){
Object data = result.getData();
JavaType javaType = TypeFactory.defaultInstance().constructType(type);
return objectMapper.convertValue(data, javaType);
}else{
throw new RemoteException(result.getCode(), result.getMessage());
}
}
}
同時(shí),創(chuàng)建一個(gè)配置類(lèi),替換原生的解碼器。
@Bean
public Decoder feignDecoder(){
return new DailyMartResponseDecoder(objectMapper);
}
這樣,在定義或調(diào)用OpenFeign接口時(shí),直接使用原生對(duì)象ProductRespDTO即可。
@FeignClient("product-service")
public interface ProductRemoteFacade {
@GetMapping("/api/product/spu/{spuId}")
ProductRespDTO getProductBySpuId(@PathVariable("spuId") Long spuId);
}
...
@Override
public void getShoppingCartDetail(Long cartId) {
ShoppingCart shoppingCart = shoppingCartRepository.find(new CartId(cartId));
ProductRespDTO productRespResult = productRemoteClient.getProductBySpuId(1L);
}
2.2 上游異常統(tǒng)一處理
在使用OpenFeign進(jìn)行遠(yuǎn)程調(diào)用時(shí),如果HTTP狀態(tài)碼為非200,OpenFeign會(huì)觸發(fā)異常解析并進(jìn)入默認(rèn)的異常解碼器feign.codec.ErrorDecoder,將業(yè)務(wù)異常包裝成FeignException。此時(shí),如果不做任何處理,調(diào)用時(shí)可以返回的消息會(huì)變成FeignException的消息體,如下所示:
圖片
顯然,這個(gè)包裝后的異常我們不需要,應(yīng)該直接將捕獲到的生產(chǎn)者的業(yè)務(wù)異常拋給前端。那么,如何解決這個(gè)問(wèn)題呢?
可以通過(guò)重寫(xiě)OpenFeign的默認(rèn)異常解碼器來(lái)實(shí)現(xiàn),代碼如下:
@RequiredArgsConstructor
@Slf4j
public class DailyMartFeignErrorDecoder implements ErrorDecoder {
private final ObjectMapper objectMapper;
/**
* OpenFeign的異常解析
* @author Java日知錄
* @param methodKey 方法名
* @param response 響應(yīng)體
*/
@Override
public Exception decode(String methodKey, Response response) {
try {
Reader reader = response.body().asReader(Charset.defaultCharset());
Result<?> result = objectMapper.readValue(reader, objectMapper.constructType(Result.class));
return new RemoteException(result.getCode(),result.getMessage());
} catch (IOException e) {
log.error("Response轉(zhuǎn)換異常",e);
throw new RemoteException(ErrorCode.FEIGN_ERROR);
}
}
}
此異常解碼器直接將異常轉(zhuǎn)化為自定義的RemoteException,表示遠(yuǎn)程調(diào)用異常。
當(dāng)然,還需要在配置類(lèi)中注入此異常解碼器。
2.3 Feign全局異常處理
在2.2小節(jié)中,我們拋出了自定義的業(yè)務(wù)異常,然而OpenFeign處理響應(yīng)時(shí)會(huì)捕獲到業(yè)務(wù)異常并將其轉(zhuǎn)換成DecodeException。
圖片
由于DailyMart中的全局異常處理器沒(méi)有單獨(dú)處理DecodeException,它會(huì)被兜底異常處理器攔截,并返回類(lèi)似“系統(tǒng)異常,請(qǐng)聯(lián)系管理員”的錯(cuò)誤提示。
因此,要完全使用上游系統(tǒng)的業(yè)務(wù)異常,還需要定義一個(gè)單獨(dú)的異常處理器來(lái)處理DecodeException。這個(gè)處理器可以與全局異常處理器分開(kāi),代碼如下:
/**
* Feign的全局異常處理,與常規(guī)的全局異常處理類(lèi)分開(kāi)
* @author Java日知錄
*/
@RestControllerAdvice
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE) // 優(yōu)先級(jí)
@ResponseStatus(code = HttpStatus.BAD_REQUEST) // 統(tǒng)一 HTTP 狀態(tài)碼
public class DailyMartFeignExceptionHandler {
@ExceptionHandler(FeignException.class)
public Result<?> handleFeignException(FeignException e) {
return new Result<Void>()
.setCode(ErrorCode.REMOTE_ERROR.getCode())
.setMessage(e.getMessage())
.setTimestamp(System.currentTimeMillis());
}
@ExceptionHandler(DecodeException.class)
public Result<?> handleDecodeException(DecodeException e) {
Throwable cause = e.getCause();
if (cause instanceof AbstractException) {
RemoteException remoteException = (RemoteException) cause;
// 上游符合全局響應(yīng)包裝約定的再次拋出即可
return new Result<Void>()
.setCode(remoteException.getCode())
.setMessage(remoteException.getMessage())
.setTimestamp(System.currentTimeMillis());
}
// 全部轉(zhuǎn)換成RemoteException
return new Result<Void>()
.setCode(ErrorCode.REMOTE_ERROR.getCode())
.setMessage(e.getMessage())
.setTimestamp(System.currentTimeMillis());
}
}
如此一來(lái),框架會(huì)自動(dòng)將業(yè)務(wù)異常傳遞給調(diào)用服務(wù),業(yè)務(wù)中也無(wú)需關(guān)心全局包裝的拆解問(wèn)題,這就是OpenFeign遠(yuǎn)程調(diào)用的最佳實(shí)踐。當(dāng)然,在DailyMart中可能有許多服務(wù)都需要遠(yuǎn)程調(diào)用,我們可以將上述內(nèi)容構(gòu)建成一個(gè)通用的Starter模塊,以便其他業(yè)務(wù)模塊共享。
圖片
小結(jié)
本文深入研究了領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)和微服務(wù)架構(gòu)中的兩個(gè)關(guān)鍵概念:防腐層(ACL)和遠(yuǎn)程調(diào)用的最佳實(shí)踐。在DDD中,我們學(xué)習(xí)了如何使用ACL來(lái)隔離外部依賴(lài),降低系統(tǒng)耦合度。在微服務(wù)架構(gòu)中,我們探討了如何通過(guò)OpenFeign來(lái)實(shí)現(xiàn)跨服務(wù)調(diào)用,并解決了全局包裝和異常處理的問(wèn)題,希望本文的內(nèi)容對(duì)您在軟件開(kāi)發(fā)項(xiàng)目中有所幫助。