Spring Cloud微服务如何设计异常处理机制

张开发
2026/4/12 9:07:54 15 分钟阅读

分享文章

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;charsetUTF-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;charsetUTF-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;charsetUTF-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;charsetUTF-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调用微服务时就可以直接捕获到异常对象从而实现向本地一样处理远程服务返回的异常对象了。

更多文章