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等多个知识点的架构资料)合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!

相关文章
相关标签/搜索