微服務中,OpenFeign應該這樣用才好!
大家好,我是飄渺。在今天的DDD與微服務系列文章中,讓我們探討如何在DDD的分層架構中調用第三方服務以及在微服務中使用OpenFeign的最佳實踐。
1. DDD中的防腐層
在應用服務中,經常需要調用外部服務接口來實現某些業務功能,這就在代碼層面引入了對外部系統的依賴。例如,下面這段轉賬的代碼邏輯需要調用外部接口服務RemoteService來獲取匯率。
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強烈依賴于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對象。
具體實現如下:
// 自定義的內部值類
@Data
public class ExchangeRateDTO {
...
}
// 稅率Facade接口
public interface ExchangeRateFacade {
ExchangeRateDTO getExchangeRate(String sourceCurrency, String targetCurrency);
}
// 稅率facade實現
@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;
}
}
通過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();
}
...
}
這樣,經過ACL改造后,ApplicationService的代碼已不再直接依賴外部的類和方法,而是依賴我們自己內部定義的值類和接口。如果未來外部服務發生任何變化,只需修改Facade類和數據轉換邏輯,而不需要修改ApplicationService的邏輯。
1.3 小結
在沒有防腐層ACL的情況下,系統需要直接依賴外部對象和外部調用接口,調用邏輯如下:
圖片
而有了防腐層ACL后,系統只需要依賴內部的值類和接口,調用邏輯如下:
圖片
2. 微服務中的遠程調用
在構建微服務時,我們經常需要跨服務調用,例如在DailyMart系統中,購物車服務需要調用商品服務以獲取商品詳細信息。理論上,我們可以遵循上述ACL的實現邏輯,在購物車模塊創建Facade接口和內部轉換類。然而,在實際開發中,由于是內部系統,差異性不太明顯,通??梢灾苯邮褂肙penFeign進行遠程調用,忽略Facade定義和內部類轉換的過程。
以下是在微服務中使用OpenFeign實現跨服務調用的過程:
- 首先,在購物車模塊的基礎設施層創建一個接口,并使用@FeignClient注解進行標注。
@FeignClient("product-service")
public interface ProductRemoteFacade {
@GetMapping("/api/product/spu/{spuId}")
Result<ProductRespDTO> getProductBySpuId(@PathVariable("spuId") Long spuId);
}
需要注意的是,我們在商品服務中對外提供的商品詳情接口定義返回的是ProductRespDTO對象,但通過OpenFeign調用時返回的是Result對象。
@Operation(summary = "查詢商品詳情")
@Parameter(name = "spuId", description = "商品spuId")
@GetMapping("/api/product/spu/{spuId}")
public ProductRespDTO getProductBySpuId(@PathVariable("spuId") Long spuId) {
return productRemoteFacade.getProductBySpuId(spuId);
}
這是因為在前文中,我們定義了一個全局的包裝類GlobalResponseBodyAdvice,會自動給所有接口封裝返回對象Result。因此,在定義Feign接口時,也需要使用Result對象來接收。如果對此邏輯不太清晰,建議參考第七章的內容。
- 在啟動類上添加@EnableFeignClient注解
@SpringBootApplication
@EnableFeignClients("com.jianzh5.dailymart.module.cart.infrastructure.acl")
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
}
- 在應用服務中注入Feign接口并使用
@Override
public void getShoppingCartDetail(Long cartId) {
ShoppingCart shoppingCart = shoppingCartRepository.find(new CartId(cartId));
Result<ProductRespDTO> productRespResult = productRemoteFacade.getProductBySpuId(1L);
// 從Result對象中獲取真實的業務對象
if(productRespResult.getCode().equals("OK")){
ProductRespDTO data = productRespResult.getData();
}
}
如上所示,我們可以看到,每次調用Feign接口都需要解析Result對象以獲取真正的業務對象。這種代碼看起來有些冗余,是否有辦法去除呢?
2.1 自定義Feign的解碼器
這時,我們可以通過重寫Feign的解碼器來實現,在解碼器中完成封裝對象的拆解。
@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());
}
}
}
同時,創建一個配置類,替換原生的解碼器。
@Bean
public Decoder feignDecoder(){
return new DailyMartResponseDecoder(objectMapper);
}
這樣,在定義或調用OpenFeign接口時,直接使用原生對象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 上游異常統一處理
在使用OpenFeign進行遠程調用時,如果HTTP狀態碼為非200,OpenFeign會觸發異常解析并進入默認的異常解碼器feign.codec.ErrorDecoder,將業務異常包裝成FeignException。此時,如果不做任何處理,調用時可以返回的消息會變成FeignException的消息體,如下所示:
圖片
顯然,這個包裝后的異常我們不需要,應該直接將捕獲到的生產者的業務異常拋給前端。那么,如何解決這個問題呢?
可以通過重寫OpenFeign的默認異常解碼器來實現,代碼如下:
@RequiredArgsConstructor
@Slf4j
public class DailyMartFeignErrorDecoder implements ErrorDecoder {
private final ObjectMapper objectMapper;
/**
* OpenFeign的異常解析
* @author Java日知錄
* @param methodKey 方法名
* @param response 響應體
*/
@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轉換異常",e);
throw new RemoteException(ErrorCode.FEIGN_ERROR);
}
}
}
此異常解碼器直接將異常轉化為自定義的RemoteException,表示遠程調用異常。
當然,還需要在配置類中注入此異常解碼器。
2.3 Feign全局異常處理
在2.2小節中,我們拋出了自定義的業務異常,然而OpenFeign處理響應時會捕獲到業務異常并將其轉換成DecodeException。
圖片
由于DailyMart中的全局異常處理器沒有單獨處理DecodeException,它會被兜底異常處理器攔截,并返回類似“系統異常,請聯系管理員”的錯誤提示。
因此,要完全使用上游系統的業務異常,還需要定義一個單獨的異常處理器來處理DecodeException。這個處理器可以與全局異常處理器分開,代碼如下:
/**
* Feign的全局異常處理,與常規的全局異常處理類分開
* @author Java日知錄
*/
@RestControllerAdvice
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE) // 優先級
@ResponseStatus(code = HttpStatus.BAD_REQUEST) // 統一 HTTP 狀態碼
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;
// 上游符合全局響應包裝約定的再次拋出即可
return new Result<Void>()
.setCode(remoteException.getCode())
.setMessage(remoteException.getMessage())
.setTimestamp(System.currentTimeMillis());
}
// 全部轉換成RemoteException
return new Result<Void>()
.setCode(ErrorCode.REMOTE_ERROR.getCode())
.setMessage(e.getMessage())
.setTimestamp(System.currentTimeMillis());
}
}
如此一來,框架會自動將業務異常傳遞給調用服務,業務中也無需關心全局包裝的拆解問題,這就是OpenFeign遠程調用的最佳實踐。當然,在DailyMart中可能有許多服務都需要遠程調用,我們可以將上述內容構建成一個通用的Starter模塊,以便其他業務模塊共享。
圖片
小結
本文深入研究了領域驅動設計(DDD)和微服務架構中的兩個關鍵概念:防腐層(ACL)和遠程調用的最佳實踐。在DDD中,我們學習了如何使用ACL來隔離外部依賴,降低系統耦合度。在微服務架構中,我們探討了如何通過OpenFeign來實現跨服務調用,并解決了全局包裝和異常處理的問題,希望本文的內容對您在軟件開發項目中有所幫助。