面向外部的服务接口,我们一般会将接口的报文形式以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 or
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
derCost, 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 {