前言
首先說一下為什么發(fā)這篇文章汽久,是這樣的鹤竭、之前和粉絲聊天的時(shí)候有聊到在采用Spring Cloud進(jìn)行微服務(wù)架構(gòu)設(shè)計(jì)時(shí),微服務(wù)之間調(diào)用時(shí)異常處理機(jī)制應(yīng)該如何設(shè)計(jì)的問題景醇。我們知道在進(jìn)行微服務(wù)架構(gòu)設(shè)計(jì)時(shí)臀稚,一個微服務(wù)一般來說不可避免地會同時(shí)面向內(nèi)部和外部提供相應(yīng)的功能服務(wù)接口。面向外部提供的服務(wù)接口三痰,會通過服務(wù)網(wǎng)關(guān)(如使用Zuul提供的apiGateway)面向公網(wǎng)提供服務(wù)吧寺,如給App客戶端提供的用戶登陸、注冊等服務(wù)接口散劫。
而面向內(nèi)部的服務(wù)接口稚机,則是在進(jìn)行微服務(wù)拆分后由于各個微服務(wù)系統(tǒng)的邊界劃定問題所導(dǎo)致的功能邏輯分散,而需要微服務(wù)之間彼此提供內(nèi)部調(diào)用接口获搏,從而實(shí)現(xiàn)一個完整的功能邏輯赖条,它是之前單體應(yīng)用中本地代碼接口調(diào)用的服務(wù)化升級拆分。例如常熙,需要在團(tuán)購系統(tǒng)中纬乍,從下單到完成一次支付,需要交易系統(tǒng)在調(diào)用訂單系統(tǒng)完成下單后再調(diào)用支付系統(tǒng)症概,從而完成一次團(tuán)購下單流程蕾额,這個時(shí)候由于交易系統(tǒng)、訂單系統(tǒng)及支付系統(tǒng)是三個不同的微服務(wù)彼城,所以為了完成這次用戶訂單诅蝶,需要App調(diào)用交易系統(tǒng)提供的外部下單接口后,由交易系統(tǒng)以內(nèi)部服務(wù)調(diào)用的方式再調(diào)用訂單系統(tǒng)和支付系統(tǒng)募壕,以完成整個交易流程调炬。如下圖所示:
這里需要說明的是,在基于SpringCloud的微服務(wù)架構(gòu)中舱馅,所有服務(wù)都是通過如consul或eureka這樣的服務(wù)中間件來實(shí)現(xiàn)的服務(wù)注冊與發(fā)現(xiàn)后來進(jìn)行服務(wù)調(diào)用的缰泡,只是面向外部的服務(wù)接口會通過網(wǎng)關(guān)服務(wù)進(jìn)行暴露,面向內(nèi)部的服務(wù)接口則在服務(wù)網(wǎng)關(guān)進(jìn)行屏蔽,避免直接暴露給公網(wǎng)棘钞。而內(nèi)部微服務(wù)間的調(diào)用還是可以直接通過consul或eureka進(jìn)行服務(wù)發(fā)現(xiàn)調(diào)用缠借,這二者并不沖突,只是外部客戶端是通過調(diào)用服務(wù)網(wǎng)關(guān)宜猜,服務(wù)網(wǎng)關(guān)通過consul再具體路由到對應(yīng)的微服務(wù)接口泼返,而內(nèi)部微服務(wù)則是直接通過consul或者eureka發(fā)現(xiàn)服務(wù)后直接進(jìn)行調(diào)用。
異常處理的差異
面向外部的服務(wù)接口姨拥,我們一般會將接口的報(bào)文形式以JSON的方式進(jìn)行響應(yīng)绅喉,除了正常的數(shù)據(jù)報(bào)文外,我們一般會在報(bào)文格式中冗余一個響應(yīng)碼和響應(yīng)信息的字段叫乌,如正常的接口成功返回:
{
"code": "0",
"msg": "success",
"data": {
"userId": "zhangsan",
"balance": 5000
}
}
而如果出現(xiàn)異巢窆蓿或者錯誤,則會相應(yīng)地返回錯誤碼和錯誤信息憨奸,如:
{
"code": "-1",
"msg": "請求參數(shù)錯誤",
"data": null
}
在編寫面向外部的服務(wù)接口時(shí)革屠,服務(wù)端所有的異常處理我們都要進(jìn)行相應(yīng)地捕獲,并在controller層映射成相應(yīng)地錯誤碼和錯誤信息膀藐,因?yàn)槊嫦蛲獠康氖侵苯颖┞督o用戶的屠阻,是需要進(jìn)行比較友好的展示和提示的,即便系統(tǒng)出現(xiàn)了異常也要堅(jiān)決向用戶進(jìn)行友好輸出额各,千萬不能輸出代碼級別的異常信息,否則用戶會一頭霧水吧恃。對于客戶端而言虾啦,只需要按照約定的報(bào)文格式進(jìn)行報(bào)文解析及邏輯處理即可,一般我們在開發(fā)中調(diào)用的第三方開放服務(wù)接口也都會進(jìn)行類似的設(shè)計(jì)痕寓,錯誤碼及錯誤信息分類得也是非常清晰傲醉!
而微服務(wù)間彼此的調(diào)用在異常處理方面,我們則是希望更直截了當(dāng)一些呻率,就像調(diào)用本地接口一樣方便硬毕,在基于Spring Cloud的微服務(wù)體系中,微服務(wù)提供方會提供相應(yīng)的客戶端SDK代碼礼仗,而客戶端SDK代碼則是通過FeignClient的方式進(jìn)行服務(wù)調(diào)用吐咳,如:而微服務(wù)間彼此的調(diào)用在異常處理方面,我們則是希望更直截了當(dāng)一些元践,就像調(diào)用本地接口一樣方便韭脊,在基于Spring Cloud的微服務(wù)體系中,微服務(wù)提供方會提供相應(yīng)的客戶端SDK代碼单旁,而客戶端SDK代碼則是通過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)然這個是FeignClient框架提供的功能蔫饰,它內(nèi)部會集成像Ribbon和Hystrix這樣的框架來實(shí)現(xiàn)客戶端服務(wù)調(diào)用的負(fù)載均衡和服務(wù)熔斷功能(注解上會指定熔斷觸發(fā)后的處理代碼類)琅豆,由于本文的主題是討論異常處理,這里暫時(shí)就不作展開了篓吁。
現(xiàn)在的問題是趋距,雖然FeignClient向服務(wù)調(diào)用方提供了類似于本地代碼調(diào)用的服務(wù)對接體驗(yàn),但服務(wù)調(diào)用方卻是不希望調(diào)用時(shí)發(fā)生錯誤的越除,即便發(fā)生錯誤节腐,如何進(jìn)行錯誤處理也是服務(wù)調(diào)用方希望知道的事情。另一方面摘盆,我們在設(shè)計(jì)內(nèi)部接口時(shí)翼雀,又不希望將報(bào)文形式搞得類似于外部接口那樣復(fù)雜,因?yàn)榇蠖鄶?shù)場景下孩擂,我們是希望服務(wù)的調(diào)用方可以直截了的獲取到數(shù)據(jù)狼渊,從而直接利用FeignClient客戶端的封裝,將其轉(zhuǎn)化為本地對象使用类垦。
@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ái)?shù)據(jù)就是設(shè)計(jì)成了一個正常的VO/BO對象的這種形式,而不是向外部接口那么樣額外設(shè)計(jì)錯誤碼或者錯誤信息之類的字段蚤认,當(dāng)然米苹,也并不是說那樣的設(shè)計(jì)方式不可以,只是感覺會讓內(nèi)部正常的邏輯調(diào)用砰琢,變得比較啰嗦和冗余蘸嘶,畢竟對于內(nèi)部微服務(wù)調(diào)用來說,要么對陪汽,要么錯训唱,錯了就Fallback邏輯就好了。
不過挚冤,話雖說如此况增,可畢竟服務(wù)是不可避免的會有異常情況的。如果內(nèi)部服務(wù)在調(diào)用時(shí)發(fā)生了錯誤训挡,調(diào)用方還是應(yīng)該知道具體的錯誤信息的澳骤,只是這種錯誤信息的提示需要以異常的方式被集成了FeignClient的服務(wù)調(diào)用方捕獲,并且不影響正常邏輯下的返回對象設(shè)計(jì)舍哄,也就是說我不想額外在每個對象中都增加兩個冗余的錯誤信息字段宴凉,因?yàn)檫@樣看起來不是那么優(yōu)雅!
既然如此表悬,那么應(yīng)該如何設(shè)計(jì)呢弥锄?
最佳實(shí)踐設(shè)計(jì)
首先,無論是內(nèi)部還是外部的微服務(wù),在服務(wù)端我們都應(yīng)該設(shè)計(jì)一個全局異常處理類籽暇,用來統(tǒng)一封裝系統(tǒng)在拋出異常時(shí)面向調(diào)用方的返回信息温治。而實(shí)現(xiàn)這樣一個機(jī)制,我們可以利用Spring提供的注解@ControllerAdvice來實(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 processRequestParameterException(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;
}
}
如上述代碼熬荆,我們在全局異常中針對內(nèi)部統(tǒng)一異常及外部統(tǒng)一異常分別作了全局處理,這樣只要服務(wù)接口拋出了這樣的異常就會被全局處理類進(jìn)行攔截并統(tǒng)一處理錯誤的返回信息绸狐。
理論上我們可以在這個全局異常處理類中卤恳,捕獲處理服務(wù)接口業(yè)務(wù)層拋出的所有異常并統(tǒng)一響應(yīng),只是那樣會讓全局異常處理類變得非常臃腫寒矿,所以從最佳實(shí)踐上考慮突琳,我們一般會為內(nèi)部和外部接口分別設(shè)計(jì)一個統(tǒng)一面向調(diào)用方的異常對象,如外部統(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ù)異常類型都統(tǒng)一轉(zhuǎn)換成了內(nèi)部服務(wù)統(tǒng)一異常對象InternalApiException了蓝牲。這樣全局異常處理類趟脂,就可以針對這個異常進(jìn)行統(tǒng)一響應(yīng)處理了。
對于外部服務(wù)調(diào)用方的處理就不多說了搞旭。而對于內(nèi)部服務(wù)調(diào)用方而言散怖,為了能夠更加優(yōu)雅和方便地實(shí)現(xiàn)異常處理,我們也需要在基于FeignClient的SDK代碼中拋出統(tǒng)一內(nèi)部服務(wù)異常對象肄渗,如:
@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í),就會強(qiáng)制要求調(diào)用方捕獲這個異常咬最,在正常情況下調(diào)用方不需要理會這個異常翎嫡,像本地調(diào)用一樣處理返回對象數(shù)據(jù)就可以了。在異常情況下永乌,則會捕獲到這個異常的信息惑申,而這個異常信息則一般在服務(wù)端全局處理類中會被設(shè)計(jì)成一個帶有錯誤碼和錯誤信息的json數(shù)據(jù),為了避免客戶端額外編寫這樣的解析代碼翅雏,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)用方增加這樣一個FeignClient解碼器,就可以在解碼器中完成錯誤消息的轉(zhuǎn)換望几。這樣绩脆,我們在通過FeignClient調(diào)用微服務(wù)時(shí)就可以直接捕獲到異常對象,從而實(shí)現(xiàn)向本地一樣處理遠(yuǎn)程服務(wù)返回的異常對象了。
最后
以上就是在利用Spring Cloud進(jìn)行微服務(wù)拆分后關(guān)于異常處理機(jī)制的一點(diǎn)分享了靴迫,因?yàn)樽罱l(fā)現(xiàn)公司項(xiàng)目在使用Spring Cloud的微服務(wù)拆分過程中惕味,這方面的處理比較混亂,所以寫一篇文章和大家一起探討下玉锌,如有更好的方式名挥,也歡迎大家給我留言一起討論!