今天和大家聊一下在采用Spring?Cloud進(jìn)行微服務(wù)架構(gòu)設(shè)計(jì)時(shí),微服務(wù)之間調(diào)用時(shí)異常處理機(jī)制應(yīng)該如何設(shè)計(jì)的問(wèn)題。我們知道在進(jìn)行微服務(wù)架構(gòu)設(shè)計(jì)時(shí),一個(gè)微服務(wù)一般來(lái)說(shuō)不可避免地會(huì)同時(shí)面向內(nèi)部和外部提供相應(yīng)的功能服務(wù)接口。面向外部提供的服務(wù)接口,會(huì)通過(guò)服務(wù)網(wǎng)關(guān)(如使用Zuul提供的apiGateway)面向公網(wǎng)提供服務(wù),如給App客戶(hù)端提供的用戶(hù)登陸、注冊(cè)等服務(wù)接口。
創(chuàng)新互聯(lián)建站是一家專(zhuān)業(yè)提供平城企業(yè)網(wǎng)站建設(shè),專(zhuān)注與成都網(wǎng)站設(shè)計(jì)、成都做網(wǎng)站、H5場(chǎng)景定制、小程序制作等業(yè)務(wù)。10年已為平城眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專(zhuān)業(yè)網(wǎng)絡(luò)公司優(yōu)惠進(jìn)行中。
而面向內(nèi)部的服務(wù)接口,則是在進(jìn)行微服務(wù)拆分后由于各個(gè)微服務(wù)系統(tǒng)的邊界劃定問(wèn)題所導(dǎo)致的功能邏輯分散,而需要微服務(wù)之間彼此提供內(nèi)部調(diào)用接口,從而實(shí)現(xiàn)一個(gè)完整的功能邏輯,它是之前單體應(yīng)用中本地代碼接口調(diào)用的服務(wù)化升級(jí)拆分。例如,需要在團(tuán)購(gòu)系統(tǒng)中,從下單到完成一次支付,需要交易系統(tǒng)在調(diào)用訂單系統(tǒng)完成下單后再調(diào)用支付系統(tǒng),從而完成一次團(tuán)購(gòu)下單流程,這個(gè)時(shí)候由于交易系統(tǒng)、訂單系統(tǒng)及支付系統(tǒng)是三個(gè)不同的微服務(wù),所以為了完成這次用戶(hù)訂單,需要App調(diào)用交易系統(tǒng)提供的外部下單接口后,由交易系統(tǒng)以?xún)?nèi)部服務(wù)調(diào)用的方式再調(diào)用訂單系統(tǒng)和支付系統(tǒng),以完成整個(gè)交易流程。如下圖所示:
這里需要說(shuō)明的是,在基于SpringCloud的微服務(wù)架構(gòu)中,所有服務(wù)都是通過(guò)如consul或eureka這樣的服務(wù)中間件來(lái)實(shí)現(xiàn)的服務(wù)注冊(cè)與發(fā)現(xiàn)后來(lái)進(jìn)行服務(wù)調(diào)用的,只是面向外部的服務(wù)接口會(huì)通過(guò)網(wǎng)關(guān)服務(wù)進(jìn)行暴露,面向內(nèi)部的服務(wù)接口則在服務(wù)網(wǎng)關(guān)進(jìn)行屏蔽,避免直接暴露給公網(wǎng)。而內(nèi)部微服務(wù)間的調(diào)用還是可以直接通過(guò)consul或eureka進(jìn)行服務(wù)發(fā)現(xiàn)調(diào)用,這二者并不沖突,只是外部客戶(hù)端是通過(guò)調(diào)用服務(wù)網(wǎng)關(guān),服務(wù)網(wǎng)關(guān)通過(guò)consul再具體路由到對(duì)應(yīng)的微服務(wù)接口,而內(nèi)部微服務(wù)則是直接通過(guò)consul或者eureka發(fā)現(xiàn)服務(wù)后直接進(jìn)行調(diào)用。
面向外部的服務(wù)接口,我們一般會(huì)將接口的報(bào)文形式以JSON的方式進(jìn)行響應(yīng),除了正常的數(shù)據(jù)報(bào)文外,我們一般會(huì)在報(bào)文格式中冗余一個(gè)響應(yīng)碼和響應(yīng)信息的字段,如正常的接口成功返回:
{
"code": "0",
"msg": "success",
"data": {
"userId": "zhangsan",
"balance": 5000
}
}
而如果出現(xiàn)異常或者錯(cuò)誤,則會(huì)相應(yīng)地返回錯(cuò)誤碼和錯(cuò)誤信息,如:
{
"code": "-1",
"msg": "請(qǐng)求參數(shù)錯(cuò)誤",
"data": null
}
在編寫(xiě)面向外部的服務(wù)接口時(shí),服務(wù)端所有的異常處理我們都要進(jìn)行相應(yīng)地捕獲,并在controller層映射成相應(yīng)地錯(cuò)誤碼和錯(cuò)誤信息,因?yàn)槊嫦蛲獠康氖侵苯颖┞督o用戶(hù)的,是需要進(jìn)行比較友好的展示和提示的,即便系統(tǒng)出現(xiàn)了異常也要堅(jiān)決向用戶(hù)進(jìn)行友好輸出,千萬(wàn)不能輸出代碼級(jí)別的異常信息,否則用戶(hù)會(huì)一頭霧水。對(duì)于客戶(hù)端而言,只需要按照約定的報(bào)文格式進(jìn)行報(bào)文解析及邏輯處理即可,一般我們?cè)陂_(kāi)發(fā)中調(diào)用的第三方開(kāi)放服務(wù)接口也都會(huì)進(jìn)行類(lèi)似的設(shè)計(jì),錯(cuò)誤碼及錯(cuò)誤信息分類(lèi)得也是非常清晰!
而微服務(wù)間彼此的調(diào)用在異常處理方面,我們則是希望更直截了當(dāng)一些,就像調(diào)用本地接口一樣方便,在基于Spring?Cloud的微服務(wù)體系中,微服務(wù)提供方會(huì)提供相應(yīng)的客戶(hù)端SDK代碼,而客戶(hù)端SDK代碼則是通過(guò)FeignClient的方式進(jìn)行服務(wù)調(diào)用,如:而微服務(wù)間彼此的調(diào)用在異常處理方面,我們則是希望更直截了當(dāng)一些,就像調(diào)用本地接口一樣方便,在基于Spring?Cloud的微服務(wù)體系中,微服務(wù)提供方會(huì)提供相應(yīng)的客戶(hù)端SDK代碼,而客戶(hù)端SDK代碼則是通過(guò)FeignClient的方式進(jìn)行服務(wù)調(diào)用,如:
@FeignClient(value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class)
public interface OrderClient {
//訂單(內(nèi))
@RequestMapping(value = "/order/createOrder", method = RequestMethod.POST)
OrderCostDetailVo orderCost(@RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime)
}
而服務(wù)的調(diào)用方在拿到這樣的SDK后就可以忽略具體的調(diào)用細(xì)節(jié),實(shí)現(xiàn)像本地接口一樣調(diào)用其他微服務(wù)的內(nèi)部接口了,當(dāng)然這個(gè)是FeignClient框架提供的功能,它內(nèi)部會(huì)集成像Ribbon和Hystrix這樣的框架來(lái)實(shí)現(xiàn)客戶(hù)端服務(wù)調(diào)用的負(fù)載均衡和服務(wù)熔斷功能(注解上會(huì)指定熔斷觸發(fā)后的處理代碼類(lèi)),由于本文的主題是討論異常處理,這里暫時(shí)就不作展開(kāi)了。
現(xiàn)在的問(wèn)題是,雖然FeignClient向服務(wù)調(diào)用方提供了類(lèi)似于本地代碼調(diào)用的服務(wù)對(duì)接體驗(yàn),但服務(wù)調(diào)用方卻是不希望調(diào)用時(shí)發(fā)生錯(cuò)誤的,即便發(fā)生錯(cuò)誤,如何進(jìn)行錯(cuò)誤處理也是服務(wù)調(diào)用方希望知道的事情。另一方面,我們在設(shè)計(jì)內(nèi)部接口時(shí),又不希望將報(bào)文形式搞得類(lèi)似于外部接口那樣復(fù)雜,因?yàn)榇蠖鄶?shù)場(chǎng)景下,我們是希望服務(wù)的調(diào)用方可以直截了的獲取到數(shù)據(jù),從而直接利用FeignClient客戶(hù)端的封裝,將其轉(zhuǎn)化為本地對(duì)象使用。
@Data
@Builder
public class OrderCostDetailVo implements Serializable {
private String orderId;
private String userId;
private int status; //1:欠費(fèi)狀態(tài);2:扣費(fèi)成功
private int orderCost;
private String currency;
private int payCost;
private int oweCost;
public OrderCostDetailVo(String orderId, String userId, int status, int orderCost, String currency, int payCost,
int oweCost) {
this.orderId = orderId;
this.userId = userId;
this.status = status;
this.orderCost = orderCost;
this.currency = currency;
this.payCost = payCost;
this.oweCost = oweCost;
}
}
如我們?cè)诎逊祷財(cái)?shù)據(jù)就是設(shè)計(jì)成了一個(gè)正常的VO/BO對(duì)象的這種形式,而不是向外部接口那么樣額外設(shè)計(jì)錯(cuò)誤碼或者錯(cuò)誤信息之類(lèi)的字段,當(dāng)然,也并不是說(shuō)那樣的設(shè)計(jì)方式不可以,只是感覺(jué)會(huì)讓內(nèi)部正常的邏輯調(diào)用,變得比較啰嗦和冗余,畢竟對(duì)于內(nèi)部微服務(wù)調(diào)用來(lái)說(shuō),要么對(duì),要么錯(cuò),錯(cuò)了就Fallback邏輯就好了。
不過(guò),話雖說(shuō)如此,可畢竟服務(wù)是不可避免的會(huì)有異常情況的。如果內(nèi)部服務(wù)在調(diào)用時(shí)發(fā)生了錯(cuò)誤,調(diào)用方還是應(yīng)該知道具體的錯(cuò)誤信息的,只是這種錯(cuò)誤信息的提示需要以異常的方式被集成了FeignClient的服務(wù)調(diào)用方捕獲,并且不影響正常邏輯下的返回對(duì)象設(shè)計(jì),也就是說(shuō)我不想額外在每個(gè)對(duì)象中都增加兩個(gè)冗余的錯(cuò)誤信息字段,因?yàn)檫@樣看起來(lái)不是那么優(yōu)雅!
既然如此,那么應(yīng)該如何設(shè)計(jì)呢?
首先,無(wú)論是內(nèi)部還是外部的微服務(wù),在服務(wù)端我們都應(yīng)該設(shè)計(jì)一個(gè)全局異常處理類(lèi),用來(lái)統(tǒng)一封裝系統(tǒng)在拋出異常時(shí)面向調(diào)用方的返回信息。而實(shí)現(xiàn)這樣一個(gè)機(jī)制,我們可以利用Spring提供的注解@ControllerAdvice來(lái)實(shí)現(xiàn)異常的全局?jǐn)r截和統(tǒng)一處理功能。如:
@Slf4j
@RestController
@ControllerAdvice
public class GlobalExceptionHandler {
@Resource
MessageSource messageSource;
@ExceptionHandler({org.springframework.web.bind.MissingServletRequestParameterException.class})
@ResponseBody
public APIResponse proce***equestParameterException(HttpServletRequest request,
HttpServletResponse response,
MissingServletRequestParameterException e) {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
APIResponse result = new APIResponse();
result.setCode(ApiResultStatus.BAD_REQUEST.getApiResultStatus());
result.setMessage(
messageSource.getMessage(ApiResultStatus.BAD_REQUEST.getMessageResourceName(),
null, LocaleContextHolder.getLocale()) + e.getParameterName());
return result;
}
@ExceptionHandler(Exception.class)
@ResponseBody
public APIResponse processDefaultException(HttpServletResponse response,
Exception e) {
//log.error("Server exception", e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
APIResponse result = new APIResponse();
result.setCode(ApiResultStatus.INTERNAL_SERVER_ERROR.getApiResultStatus());
result.setMessage(messageSource.getMessage(ApiResultStatus.INTERNAL_SERVER_ERROR.getMessageResourceName(), null,
LocaleContextHolder.getLocale()));
return result;
}
@ExceptionHandler(ApiException.class)
@ResponseBody
public APIResponse processApiException(HttpServletResponse response,
ApiException e) {
APIResponse result = new APIResponse();
response.setStatus(e.getApiResultStatus().getHttpStatus());
response.setContentType("application/json;charset=UTF-8");
result.setCode(e.getApiResultStatus().getApiResultStatus());
String message = messageSource.getMessage(e.getApiResultStatus().getMessageResourceName(),
null, LocaleContextHolder.getLocale());
result.setMessage(message);
//log.error("Knowned exception", e.getMessage(), e);
return result;
}
/**
* 內(nèi)部微服務(wù)異常統(tǒng)一處理方法
*/
@ExceptionHandler(InternalApiException.class)
@ResponseBody
public APIResponse processMicroServiceException(HttpServletResponse response,
InternalApiException e) {
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json;charset=UTF-8");
APIResponse result = new APIResponse();
result.setCode(e.getCode());
result.setMessage(e.getMessage());
return result;
}
}
如上述代碼,我們?cè)谌之惓V嗅槍?duì)內(nèi)部統(tǒng)一異常及外部統(tǒng)一異常分別作了全局處理,這樣只要服務(wù)接口拋出了這樣的異常就會(huì)被全局處理類(lèi)進(jìn)行攔截并統(tǒng)一處理錯(cuò)誤的返回信息。
理論上我們可以在這個(gè)全局異常處理類(lèi)中,捕獲處理服務(wù)接口業(yè)務(wù)層拋出的所有異常并統(tǒng)一響應(yīng),只是那樣會(huì)讓全局異常處理類(lèi)變得非常臃腫,所以從最佳實(shí)踐上考慮,我們一般會(huì)為內(nèi)部和外部接口分別設(shè)計(jì)一個(gè)統(tǒng)一面向調(diào)用方的異常對(duì)象,如外部統(tǒng)一接口異常我們叫ApiException,而內(nèi)部統(tǒng)一接口異常叫InternalApiException。這樣,我們就需要在面向外部的服務(wù)接口controller層中,將所有的業(yè)務(wù)異常轉(zhuǎn)換為ApiException;而在面向內(nèi)部服務(wù)的controller層中將所有的業(yè)務(wù)異常轉(zhuǎn)化為InternalApiException。如:
@RequestMapping(value = "/creatOrder", method = RequestMethod.POST)
public OrderCostDetailVo orderCost(
@RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime)throws InternalApiException {
OrderCostVo costVo = OrderCostVo.builder().orderId(orderId).userId(userId).busiId(busiId).orderType(orderType)
.duration(duration).bikeType(bikeType).bikeNo(bikeNo).cityId(cityId).orderCost(orderCost)
.currency(currency).strategyId(strategyId).tradeTime(tradeTime).countryName(countryName)
.build();
OrderCostDetailVo orderCostDetailVo;
try {
orderCostDetailVo = orderCostServiceImpl.orderCost(costVo);
return orderCostDetailVo;
} catch (VerifyDataException e) {
log.error(e.toString());
throw new InternalApiException(e.getCode(), e.getMessage());
} catch (RepeatDeductException e) {
log.error(e.toString());
throw new InternalApiException(e.getCode(), e.getMessage());
}
}
如上面的內(nèi)部服務(wù)接口的controller層中將所有的業(yè)務(wù)異常類(lèi)型都統(tǒng)一轉(zhuǎn)換成了內(nèi)部服務(wù)統(tǒng)一異常對(duì)象InternalApiException了。這樣全局異常處理類(lèi),就可以針對(duì)這個(gè)異常進(jìn)行統(tǒng)一響應(yīng)處理了。
對(duì)于外部服務(wù)調(diào)用方的處理就不多說(shuō)了。而對(duì)于內(nèi)部服務(wù)調(diào)用方而言,為了能夠更加優(yōu)雅和方便地實(shí)現(xiàn)異常處理,我們也需要在基于FeignClient的SDK代碼中拋出統(tǒng)一內(nèi)部服務(wù)異常對(duì)象,如:
@FeignClient(value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class)
public interface OrderClient {
//訂單(內(nèi))
@RequestMapping(value = "/order/createOrder", method = RequestMethod.POST)
OrderCostDetailVo orderCost(@RequestParam(value = "orderId") String orderId,
@RequestParam(value = "userId") long userId,
@RequestParam(value = "orderType") String orderType,
@RequestParam(value = "orderCost") int orderCost,
@RequestParam(value = "currency") String currency,
@RequestParam(value = "tradeTime") String tradeTime)throws InternalApiException};
這樣在調(diào)用方進(jìn)行調(diào)用時(shí),就會(huì)強(qiáng)制要求調(diào)用方捕獲這個(gè)異常,在正常情況下調(diào)用方不需要理會(huì)這個(gè)異常,像本地調(diào)用一樣處理返回對(duì)象數(shù)據(jù)就可以了。在異常情況下,則會(huì)捕獲到這個(gè)異常的信息,而這個(gè)異常信息則一般在服務(wù)端全局處理類(lèi)中會(huì)被設(shè)計(jì)成一個(gè)帶有錯(cuò)誤碼和錯(cuò)誤信息的json數(shù)據(jù),為了避免客戶(hù)端額外編寫(xiě)這樣的解析代碼,FeignClient為我們提供了異常解碼機(jī)制。如:
@Slf4j
@Configuration
public class FeignClientErrorDecoder implements feign.codec.ErrorDecoder {
private static final Gson gson = new Gson();
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() != HttpStatus.OK.value()) {
if (response.status() == HttpStatus.SERVICE_UNAVAILABLE.value()) {
String errorContent;
try {
errorContent = Util.toString(response.body().asReader());
InternalApiException internalApiException = gson.fromJson(errorContent, InternalApiException.class);
return internalApiException;
} catch (IOException e) {
log.error("handle error exception");
return new InternalApiException(500, "unknown error");
}
}
}
return new InternalApiException(500, "unknown error");
}
}
我們只需要在服務(wù)調(diào)用方增加這樣一個(gè)FeignClient解碼器,就可以在解碼器中完成錯(cuò)誤消息的轉(zhuǎn)換。這樣,我們?cè)谕ㄟ^(guò)FeignClient調(diào)用微服務(wù)時(shí)就可以直接捕獲到異常對(duì)象,從而實(shí)現(xiàn)向本地一樣處理遠(yuǎn)程服務(wù)返回的異常對(duì)象了。
以上就是在利用Spring?Cloud進(jìn)行微服務(wù)拆分后關(guān)于異常處理機(jī)制的一點(diǎn)分享了,因?yàn)樽罱l(fā)現(xiàn)公司項(xiàng)目在使用Spring?Cloud的微服務(wù)拆分過(guò)程中,這方面的處理比較混亂,所以寫(xiě)一篇文章和大家一起探討下,如有更好的方式,也歡迎大家給我留言一起討論!