导读
今天和⼤家聊⼀下在采⽤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\public interface OrderClient { //订单(内)
@RequestMapping(value = \"/order/createOrder\ 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\
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\ 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\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\public interface OrderClient { //订单(内)
@RequestMapping(value = \"/order/createOrder\ 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的微服务拆分过程中,这⽅⾯的处理⽐较混乱,所以写⼀篇⽂章和⼤家⼀起探讨下,如有更好的⽅式,也欢迎⼤家给我留⾔!
因篇幅问题不能全部显示,请点此查看更多更全内容