乐趣区

花了三天整理Spring-Cloud微服务如何设计异常处理机制还看不懂算我输

前言

首先说一下为什么发这篇文章,是这样的、之前和粉丝聊天的时候有聊到在采纳 Spring Cloud 进行微服务架构设计时,微服务之间调用时异样解决机制应该如何设计的问题。咱们晓得在进行微服务架构设计时,一个微服务一般来说不可避免地会同时面向外部和内部提供相应的性能服务接口。面向内部提供的服务接口,会通过服务网关(如应用 Zuul 提供的 apiGateway)面向公网提供服务,如给 App 客户端提供的用户登陆、注册等服务接口。

而面向外部的服务接口,则是在进行微服务拆分后因为各个微服务零碎的边界划定问题所导致的性能逻辑扩散,而须要微服务之间彼此提供外部调用接口,从而实现一个残缺的性能逻辑,它是之前单体利用中本地代码接口调用的服务化降级拆分。例如,须要在团购零碎中,从下单到实现一次领取,须要交易系统在调用订单零碎实现下单后再调用领取零碎,从而实现一次团购下单流程,这个时候因为交易系统、订单零碎及领取零碎是三个不同的微服务,所以为了实现这次用户订单,须要 App 调用交易系统提供的内部下单接口后,由交易系统以外部服务调用的形式再调用订单零碎和领取零碎,以实现整个交易流程。如下图所示:

这里须要阐明的是,在基于 SpringCloud 的微服务架构中,所有服务都是通过如 consul 或 eureka 这样的服务中间件来实现的服务注册与发现起初进行服务调用的,只是面向内部的服务接口会通过网关服务进行裸露,面向外部的服务接口则在服务网关进行屏蔽,防止间接裸露给公网。而外部微服务间的调用还是能够间接通过 consul 或 eureka 进行服务发现调用,这二者并不抵触,只是 内部客户端是通过调用服务网关,服务网关通过 consul 再具体路由到对应的微服务接口,而外部微服务则是间接通过 consul 或者 eureka 发现服务后间接进行调用

异样解决的差别

面向内部的服务接口,咱们个别会将接口的报文模式以 JSON 的形式进行响应,除了失常的数据报文外,咱们个别会在报文格式中冗余一个响应码和响应信息的字段,如失常的接口胜利返回:

{
    "code": "0",
    "msg": "success",
    "data": {
        "userId": "zhangsan",
        "balance": 5000
    }
}

而如果出现异常或者谬误,则会相应地返回错误码和错误信息,如:

{
    "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 这样的框架来实现客户端服务调用的负载平衡和服务熔断性能(注解上会指定熔断触发后的解决代码类),因为本文的主题是探讨异样解决,这里临时就不作开展了。

当初的问题是,尽管 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 的微服务拆分过程中,这方面的解决比拟凌乱,所以写一篇文章和大家一起探讨下,如有更好的形式,也欢送大家给我留言一起探讨!

退出移动版