最近團(tuán)隊(duì)從dubbo切換到springcloud件炉,自己碰到的一些問(wèn)題,特別是這個(gè)很常見(jiàn)的調(diào)用異常愕撰,做一些分析刹衫。
springcloud 微服務(wù)框架有各種組件,可以搭建一個(gè)完整的微服務(wù)應(yīng)用搞挣。包括
注冊(cè) 中心:eureka 或者 consul
服務(wù)提供者:各種 provider
服務(wù)消費(fèi)者:各種 consumer
網(wǎng) 關(guān):zuul
這里一定會(huì)碰到的問(wèn)題就是, consumer 調(diào)用 provider, provider 內(nèi)部出現(xiàn)異常了带迟,consumer 如何拿到具體的異常信息并且返回給頁(yè)面。
為什么要返回具體的異常信息囱桨?
異常不具體的話仓犬,什么都是提示服務(wù)器異常,那說(shuō)了等于沒(méi)說(shuō)舍肠,誰(shuí)特么不知道是服務(wù)器異常搀继。如果提示具體信息的話,比如訂單號(hào)已存在翠语,訂單不存在叽躯,扣費(fèi)失敗,sku不存在肌括,一眼就可以定位到問(wèn)題在哪里点骑,不用去翻日志找半天。
現(xiàn)成的方案就是使用 hystrix 斷路器的功能
斷路器的使用 :
1 設(shè)置 fallback
@FeignClient(name = ProviderServiceName.SERVICE_NAME, fallbackFactory=CommentServiceFallbackFactory.class)
public interface CommentService extends CommentBridge {
//現(xiàn)在測(cè)試 調(diào)用這個(gè)方法谍夭,provider出現(xiàn)異常
@PostMapping(value="/save")
void save(@RequestBody Comment comment);
}
save 方法沒(méi)有返回值說(shuō)明:
對(duì)于這種可以沒(méi)有返回值的方法調(diào)用黑滴,有些人認(rèn)為要加上返回值 response, 然后在消費(fèi)者的代碼里面來(lái)判斷返回值是否成功。其實(shí)大可不必紧索,微服務(wù)之間的調(diào)用也是服務(wù)調(diào)用袁辈,相當(dāng)于調(diào)用一個(gè)方法而已,沒(méi)有拋出異常就可以認(rèn)為是執(zhí)行成功的齐板,有異常的話都程序停止執(zhí)行吵瞻,事務(wù)回滾了葛菇。為什么還要加 response, 在里面搞一個(gè)所謂的 狀態(tài)碼來(lái)判斷呢甘磨,它是微服務(wù),你卻把它當(dāng)成 http rest 接口來(lái)使用,它有的功能你不用眯停,這完全就是沒(méi)有領(lǐng)會(huì)微服務(wù)的概念济舆。
2 編寫(xiě) fallback 類
@Component
@Slf4j
public class CommentServiceFallbackFactory implements feign.hystrix.FallbackFactory<CommentService> {
@Override
public CommentService create(Throwable cause) {
//cause是調(diào)用時(shí)出現(xiàn)的異常信息
final String message = cause.getMessage();
return new CommentService() {
@Override
public Page<Comment> page(CommentParameter parameter) {
return null;
}
@Override
public void save(Comment comment) {
log.error("進(jìn)程pid: " + ManagementFactory.getRuntimeMXBean().getName());
log.error("線程: " + Thread.currentThread().getName());
//這里拋出異常嘗試消費(fèi)者的全局異常處理器捕獲
throw new BizException(message);
}
};
}
}
3 嘗試捕獲 BizException(message)
@Component
@Slf4j
public class GlobalExceptionHandler implements HandlerExceptionResolver, Ordered {
private static final String ERROR_MESSAGE = "服務(wù)器掛掉了";
@Override
public int getOrder() {
return 0;
}
@ResponseBody
@Nullable
@Override
public ModelAndView resolveException(HttpServletRequest request,HttpServletResponse response,
@Nullable Object handler, Exception ex) {
log.error("進(jìn)程pid: " + ManagementFactory.getRuntimeMXBean().getName());
log.error("線程: " + Thread.currentThread().getName());
}
}
4 服務(wù)提供者里面直接拋異常
@RestController
public class CommentProviderController implements CommentBridge {
@Resource
private CommentRepository commentRepository;
@Override
public void save(Comment comment) {
throw new BizException("provider service 拋出的異常");
}
經(jīng)過(guò)測(cè)試,CommentServiceFallbackFactory 的 save 方法拋出的異常是無(wú)法被捕獲器捕獲到的莺债,這樣就沒(méi)法通過(guò) fallback 方法去控制異常的展示滋觉,返回签夭。
如果按照上面的方式來(lái)使用斷路器的話,這種使用方式完全是不可用的我纪,一個(gè)是 每個(gè)服務(wù)類里面要配置 fallbackFactory 慎宾,有多少個(gè)服務(wù)類就要對(duì)應(yīng)的寫(xiě)多少個(gè)回滾類,寫(xiě)到你吐血浅悉。第二個(gè)是斷路器里面拿到的 provider 的異常信息趟据,如何傳遞給消費(fèi)者,可以考慮用線程的等待通知機(jī)制术健,但是這么玩就不是微服務(wù)了汹碱。
現(xiàn)在有2種方式可以讓具體的異常信息逐級(jí)上報(bào),返回給頁(yè)面
1 全局異常處理里面解析捕獲到的異常信息荞估,直接返回到頁(yè)面
@Component
@Slf4j
public class GlobalExceptionHandler implements HandlerExceptionResolver, Ordered {
private static final String ERROR_MESSAGE = "服務(wù)器掛掉了";
@Override
public int getOrder() {
return 0;
}
@ResponseBody
@Nullable
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex) {
log.error("進(jìn)程pid: " + ManagementFactory.getRuntimeMXBean().getName());
log.error("線程: " + Thread.currentThread().getName());
ModelAndView modelAndView = new ModelAndView();
MappingJackson2JsonView mappingJackson2JsonView = new MappingJackson2JsonView();
Map<String, Object> attributes = new HashMap<>(2);
attributes.put("succeed", false);
String errorMessage = null;
if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException validException = (MethodArgumentNotValidException) ex;
FieldError fieldError = validException.getBindingResult().getFieldErrors().get(0);
errorMessage = fieldError.getField() + " " + fieldError.getDefaultMessage();
}
else if (ex instanceof HystrixRuntimeException) {
Throwable throwable = ex.getCause();
if (throwable instanceof FeignException) {
String content = StringUtils.substringBetween(throwable.getMessage(), "{", "}");
JsonObject jsonpObject = new JsonParser().parse("{" + content + "}")
.getAsJsonObject();
errorMessage = jsonpObject.get("message").getAsString();
}
}
else {
errorMessage = ERROR_MESSAGE;
}
attributes.put("message", errorMessage);
mappingJackson2JsonView.setAttributesMap(attributes);
modelAndView.setView(mappingJackson2JsonView);
return modelAndView;
}
}
2 配置 feign 的 ErrorDecoder
@Component
public class FeignErrorDecoder implements ErrorDecoder{
@Override
public Exception decode(String methodKey, Response response){
//在這里解析 response 的結(jié)果并返回的異常信息可以被全局異常處理捕獲到
return new Exception(response.getMessage());
}
}
最終的效果就是這樣,調(diào)用消費(fèi)者的接口咳促,消費(fèi)者再去調(diào)用 提供者的接口,提供者處理時(shí)出現(xiàn)異常(參考上面第四部那個(gè)圖)
把異常信息作為結(jié)果返回