Spring Cloud微服務如何設計異常處理機制?

 導讀程序員

今天和你們聊一下在採用Spring Cloud進行微服務架構設計時,微服務之間調用時異常處理機制應該如何設計的問題。咱們知道在進行微服務架構設計時,一個微服務通常來講不可避免地會同時面向內部和外部提供相應的功能服務接口。面向外部提供的服務接口,會經過服務網關(如使用Zuul提供的apiGateway)面向公網提供服務,如給App客戶端提供的用戶登錄、註冊等服務接口。web

而面向內部的服務接口,則是在進行微服務拆分後因爲各個微服務系統的邊界劃定問題所致使的功能邏輯分散,而須要微服務之間彼此提供內部調用接口,從而實現一個完整的功能邏輯,它是以前單體應用中本地代碼接口調用的服務化升級拆分。例如,須要在團購系統中,從下單到完成一次支付,須要交易系統在調用訂單系統完成下單後再調用支付系統,從而完成一次團購下單流程,這個時候因爲交易系統、訂單系統及支付系統是三個不一樣的微服務,因此爲了完成此次用戶訂單,須要App調用交易系統提供的外部下單接口後,由交易系統之內部服務調用的方式再調用訂單系統和支付系統,以完成整個交易流程。以下圖所示:spring


這裏須要說明的是,在基於SpringCloud的微服務架構中,全部服務都是經過如consul或eureka這樣的服務中間件來實現的服務註冊與發現後來進行服務調用的,只是面向外部的服務接口會經過網關服務進行暴露,面向內部的服務接口則在服務網關進行屏蔽 ,避免直接暴露給公網。而內部微服務間的調用仍是能夠直接經過consul或eureka進行服務發現調用,這兩者並不衝突,只是 外部客戶端是經過調用服務網關,服務網關經過consul再具體路由到對應的微服務接口,而內部微服務則是直接經過consul或者eureka發現服務後直接進行調用 。sql

異常處理的差別json

面向外部的服務接口,咱們通常會將接口的報文形式以JSON的方式進行響應,除了正常的數據報文外,咱們通常會在報文格式中冗餘一個響應碼和響應信息的字段,如正常的接口成功返回: api

{
    "code": "0",
    "msg": "success",
    "data": {
        "userId": "zhangsan",
        "balance": 5000
    }
}
複製代碼

而若是出現異常或者錯誤,則會相應地返回錯誤碼和錯誤信息,如: bash

{
    "code": "-1",
    "msg": "請求參數錯誤",
    "data": null
}
複製代碼

在編寫面向外部的服務接口時,服務端全部的異常處理咱們都要進行相應地捕獲,並在controller層映射成相應地錯誤碼和錯誤信息,由於面向外部的是直接暴露給用戶的,是須要進行比較友好的展現和提示的,即使系統出現了異常也要堅定向用戶進行友好輸出,千萬不能輸出代碼級別的異常信息,不然用戶會一頭霧水。對於客戶端而言,只須要按照約定的報文格式進行報文解析及邏輯處理便可,通常咱們在開發中調用的第三方開放服務接口也都會進行相似的設計,錯誤碼及錯誤信息分類得也是很是清晰!架構

而微服務間彼此的調用在異常處理方面,咱們則是但願更直截了當一些,就像調用本地接口同樣方便,在基於Spring Cloud的微服務體系中,微服務提供方會提供相應的客戶端SDK代碼,而客戶端SDK代碼則是經過FeignClient的方式進行服務調用,如: 而微服務間彼此的調用在異常處理方面,咱們則是但願更直截了當一些,就像調用本地接口同樣方便,在基於Spring Cloud的微服務體系中,微服務提供方會提供相應的客戶端SDK代碼,而客戶端SDK代碼則是經過FeignClient的方式進行服務調用,如:併發

@FeignClient(value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class)
public interface OrderClient {
    //訂單(內)
    @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)
}
複製代碼

而服務的調用方在拿到這樣的SDK後就能夠忽略具體的調用細節,實現像本地接口同樣調用其餘微服務的內部接口了,固然這個是FeignClient框架提供的功能, 它內部會集成像Ribbon和Hystrix這樣的框架來實現客戶端服務調用的負載均衡和服務熔斷功能 (註解上會指定熔斷觸發後的處理代碼類),因爲本文的主題是討論異常處理,這裏暫時就不做展開了。 app

如今的問題是,雖然FeignClient向服務調用方提供了相似於本地代碼調用的服務對接體驗,但服務調用方倒是不但願調用時發生錯誤的,即使發生錯誤,如何進行錯誤處理也是服務調用方但願知道的事情。另外一方面,咱們 在設計內部接口時,又不但願將報文形式搞得相似於外部接口那樣複雜 ,由於大多數場景下,咱們是但願服務的調用方能夠直截了的獲取到數據,從而直接利用FeignClient客戶端的封裝,將其轉化爲本地對象使用。

@Data
@Builder
public class OrderCostDetailVo implements Serializable {
    private String orderId;
    private String userId;
    private int status;   //1:欠費狀態;2:扣費成功
    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;
    }
}
複製代碼

如咱們在把返回數據就是設計成了一個正常的VO/BO對象的這種形式,而不是向外部接口那麼樣額外設計錯誤碼或者錯誤信息之類的字段,固然,也並非說那樣的設計方式不能夠,只是感受會讓內部正常的邏輯調用,變得比較囉嗦和冗餘,畢竟對於內部微服務調用來講,要麼對,要麼錯,錯了就Fallback邏輯就行了。

不過,話雖然說如此,可畢竟 服務是不可避免的會有異常狀況的 。若是內部服務在調用時發生了錯誤,調用方仍是應該知道具體的錯誤信息的,只是這種錯誤信息的提示須要以異常的方式被集成了FeignClient的服務調用方捕獲,而且不影響正常邏輯下的返回對象設計,也就是說 我不想額外在每一個對象中都增長兩個冗餘的錯誤信息字段,由於這樣看起來不是那麼優雅!

既然如此,那麼應該如何設計呢?

最佳實踐設計

首先,不管是內部仍是外部的微服務,在服務端咱們都 應該設計一個全局異常處理類 ,用來統一封裝系統在拋出異常時面向調用方的返回信息。而實現這樣一個機制,咱們能夠利用Spring提供的註解 @ControllerAdvice 來實現異常的全局攔截和統一處理功能。如:

@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;
    }
    /**
     * 內部微服務異常統一處理方法
     */
    @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;
    }
}
複製代碼

如上述代碼,咱們在全局異常中針對內部統一異常及外部統一異常分別做了全局處理,這樣只要服務接口拋出了這樣的異常就會被全局處理類進行攔截並統一處理錯誤的返回信息。

理論上咱們能夠在這個全局異常處理類中,捕獲處理服務接口業務層拋出的全部異常並統一響應,只是 那樣會讓全局異常處理類變得很是臃腫 ,因此從最佳實踐上考慮,咱們通常 會爲內部和外部接口分別設計一個統一面向調用方的異常對象, 如外部統一接口異常咱們叫ApiException,而內部統一接口異常叫InternalApiException。這樣,咱們就須要在面向外部的服務接口controller層中,將全部的業務異常轉換爲ApiException;而在面向內部服務的controller層中將全部的業務異常轉化爲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());
        } 
}
複製代碼

如上面的內部服務接口的controller層中將全部的業務異常類型都統一轉換成了內部服務統一異常對象InternalApiException了。這樣全局異常處理類,就能夠針對這個異常進行統一響應處理了。

對於外部服務調用方的處理就很少說了。而對於內部服務調用方而言,爲了可以更加優雅和方便地實現異常處理,咱們也須要在基於FeignClient的SDK代碼中拋出統一內部服務異常對象,如:

@FeignClient(value = "order", configuration = OrderClientConfiguration.class, fallback = OrderClientFallback.class)
public interface OrderClient {
    //訂單(內)
    @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};
複製代碼

這樣在調用方進行調用時,就會強制要求調用方捕獲這個異常,在正常狀況下調用方不須要理會這個異常,像本地調用同樣處理返回對象數據就能夠了。在異常狀況下,則會捕獲到這個異常的信息,而這個異常信息則通常在服務端全局處理類中會被設計成一個帶有錯誤碼和錯誤信息的json數據,爲了不客戶端額外編寫這樣的解析代碼, FeignClient爲咱們提供了異常解碼機制 。如:

@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");
    }
}
複製代碼

咱們只須要在 服務調用方增長這樣一個FeignClient解碼器,就能夠在解碼器中完成錯誤消息的轉換 。這樣,咱們在經過FeignClient調用微服務時就能夠直接捕獲到異常對象,從而 實現向本地同樣處理遠程服務返回的異常對象了

以上就是在利用Spring Cloud進行微服務拆分後關於異常處理機制的一點分享了,由於最近發現公司項目在使用Spring Cloud的微服務拆分過程當中,這方面的處理比較混亂,因此寫一篇文章和你們一塊兒探討下,若有更好的方式,也歡迎你們給我留言!

歡迎工做一到五年的Java工程師朋友們加入Java程序員開發: 721575865

羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!

相關文章
相關標籤/搜索