Spring Cloud 统一异常处理和统一返回
1、前言
在业务系统中,我们一般希望所有请求放回的类型都是固定的,如:{"code":0,"message":"",data:{"id":1,"name":"zhangsan"}}, 用code表示成功还是失败,message记录失败信息,如果成功,用data返回具体的数据。为了满足这样的需求,我们必须在每个Controller都包装try catch,返回异常信息,同时所有的请求的返回对于都是该对象。有没有更好的办法解决上述问题。
2、Spring cloud 项目一般架构
从图可以发现所有的外部请求都是通过gateway进行的,因此具体业务根本不需要进行任何修改,只需在gateway中进行修改返回值。
3、实现
返回类:
/** * 响应对象 * * @param <T> */ public class ResponseVo<T> { /** * 状态码. */ private Integer code; /** * 提示信息. */ private String msg; /** * 具体的数据. */ private T data; public ResponseVo() { } public ResponseVo(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseVo(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } @Override public String toString() { return "ResponseVo{" + "code=" + code + ", msg='" + msg + '\'' + ", data=" + data + '}'; } }
修复返回对象工具
public class BodyUtils { private static final Log log = LogFactory.getLog(BodyUtils.class); private final static int OK = 0; public final static String transforBody(String body) { ResponseVo responseVo = new ResponseVo(); responseVo.setCode(OK); if (StringUtils.isEmpty(body)) { responseVo.setData(body); return JSON.toJSONString(responseVo); } if (!(body.startsWith("{") && body.endsWith("}")) && !(body.startsWith("[") && body.endsWith("]"))) { responseVo.setData(body); return JSON.toJSONString(responseVo); } Object jsonObject = JSON.parse(body); if (jsonObject instanceof JSONObject) { Object code = ((JSONObject) jsonObject).get("code"); if (code != null) { if (((JSONObject) jsonObject).get("data") != null) { if (((JSONObject) jsonObject).get("data") instanceof JSONObject) { JSONObject data = (JSONObject) ((JSONObject) jsonObject).get("data"); Object total = data.get("total"); Object result = data.get("result"); if (total != null && result != null) { Map response = new HashMap<>(); for (Map.Entry<String, Object> entry : data.entrySet()) { if (entry.getKey().equals("countColumn") || entry.getKey().equals("startRow") || entry.getKey().equals("reasonable") || entry.getKey().equals("count") || entry.getKey().equals("endRow") || entry.getKey().equals("orderBy") || entry.getKey().equals("pageSize") || entry.getKey().equals("pageNum") || entry.getKey().equals("empty") || entry.getKey().equals("pages") || entry.getKey().equals("orderByOnly") || entry.getKey().equals("pageSizeZero") ) { continue; } else { response.put(entry.getKey(), entry.getValue()); } } responseVo.setData(response); return JSON.toJSONString(responseVo); } else { return body; } } else { return body; } } else { return body; } } else { responseVo.setData(jsonObject); return JSON.toJSONString(responseVo); } } else { responseVo.setData(jsonObject); return JSON.toJSONString(responseVo); } } }
修改gateway 中 ModifyResponseBodyGatewayFilterFactory 并覆盖原包中的类
public class ModifyResponseBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyResponseBodyGatewayFilterFactory.Config> { private final ServerCodecConfigurer codecConfigurer; public ModifyResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) { super(Config.class); this.codecConfigurer = codecConfigurer; } @Override public GatewayFilter apply(Config config) { return new ModifyResponseGatewayFilter(config); } public class ModifyResponseGatewayFilter implements GatewayFilter, Ordered { private final Config config; public ModifyResponseGatewayFilter(Config config) { this.config = config; } @Override @SuppressWarnings("unchecked") public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { Class inClass = config.getInClass(); Class outClass = config.getOutClass(); String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); HttpHeaders httpHeaders = new HttpHeaders(); //explicitly add it in this way instead of 'httpHeaders.setContentType(originalResponseContentType)' //this will prevent exception in case of using non-standard media types like "Content-Type: image" httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType); ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders); DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults()); //TODO: flux or mono Mono modifiedBody = clientResponse.bodyToMono(inClass) .flatMap(originalBody -> config.rewriteFunction.apply(exchange, originalBody)) .switchIfEmpty(Mono.just(JSON.toJSONString(new ResponseVo(0, null)))); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders()); return bodyInserter.insert(outputMessage, new BodyInserterContext()) .then(Mono.defer(() -> { Flux<DataBuffer> messageBody = outputMessage.getBody(); HttpHeaders headers = getDelegate().getHeaders(); if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { messageBody = messageBody.doOnNext(data -> headers.setContentLength(data.readableByteCount())); } //TODO: use isStreamingMediaType? return getDelegate().writeWith(messageBody); })); } @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return writeWith(Flux.from(body) .flatMapSequential(p -> p)); } }; return chain.filter(exchange.mutate().response(responseDecorator).build()); } @Override public int getOrder() { return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1; } } public class ResponseAdapter implements ClientHttpResponse { private final Flux<DataBuffer> flux; private final HttpHeaders headers; public ResponseAdapter(Publisher<? extends DataBuffer> body, HttpHeaders headers) { this.headers = headers; if (body instanceof Flux) { flux = (Flux) body; } else { flux = ((Mono) body).flux(); } } @Override public Flux<DataBuffer> getBody() { return flux; } @Override public HttpHeaders getHeaders() { return headers; } @Override public HttpStatus getStatusCode() { return null; } @Override public int getRawStatusCode() { return 0; } @Override public MultiValueMap<String, ResponseCookie> getCookies() { return null; } } public static class Config { private Class inClass = String.class; private Class outClass = String.class; private Map<String, Object> inHints; private Map<String, Object> outHints; private String newContentType; private RewriteFunction rewriteFunction = (exchange, body) -> { ServerHttpResponse response = ((ServerWebExchange) exchange).getResponse(); if (response.getHeaders().get("Content-Type")!=null) { for (String s : response.getHeaders().get("Content-Type")) { if(s.equals("application/vnd.ms-excel;charset=utf-8")){ return Mono.just(body); } } } if (body instanceof String) { return Mono.just(BodyUtils.transforBody((String) body)); } else { return Mono.just(body); } }; public Class getInClass() { return inClass; } public Config setInClass(Class inClass) { this.inClass = inClass; return this; } public Class getOutClass() { return outClass; } public Config setOutClass(Class outClass) { this.outClass = outClass; return this; } public Map<String, Object> getInHints() { return inHints; } public Config setInHints(Map<String, Object> inHints) { this.inHints = inHints; return this; } public Map<String, Object> getOutHints() { return outHints; } public Config setOutHints(Map<String, Object> outHints) { this.outHints = outHints; return this; } public String getNewContentType() { return newContentType; } public Config setNewContentType(String newContentType) { this.newContentType = newContentType; return this; } public RewriteFunction getRewriteFunction() { return rewriteFunction; } public <T, R> Config setRewriteFunction(Class<T> inClass, Class<R> outClass, RewriteFunction<T, R> rewriteFunction) { setInClass(inClass); setOutClass(outClass); setRewriteFunction(rewriteFunction); return this; } public Config setRewriteFunction(RewriteFunction rewriteFunction) { this.rewriteFunction = rewriteFunction; return this; } } }
修改地方
private RewriteFunction rewriteFunction = (exchange, body) -> { ServerHttpResponse response = ((ServerWebExchange) exchange).getResponse(); if (response.getHeaders().get("Content-Type")!=null) { for (String s : response.getHeaders().get("Content-Type")) { if(s.equals("application/vnd.ms-excel;charset=utf-8")){ return Mono.just(body); } } } if (body instanceof String) { return Mono.just(BodyUtils.transforBody((String) body)); } else { return Mono.just(body); } };
为了满足返回对象是void 的也能返回ResponseVo 还得修改
Mono modifiedBody = clientResponse.bodyToMono(inClass) .flatMap(originalBody -> config.rewriteFunction.apply(exchange, originalBody)) .switchIfEmpty(Mono.just(JSON.toJSONString(new ResponseVo(0, null))));
这地方需要加switchIfEmpty ,因为void没有返回值,所有必须加次判断。
gateway 配置
spring: cloud: gateway: routes: - id: base uri: lb://base predicates: - Path=/basal/* filters: - StripPrefix=1 - id: job uri: lb://job predicates: - Path=/job/** filters: - ModifyResponseBody - StripPrefix=1
其中配置 - ModifyResponseBody的请求都会改写返回值。
4、应用中的异常统一处理
@ControllerAdvice public class ExceptionHandler { private final static Logger logger = LoggerFactory.getLogger(ExceptionHandler.class); @ExceptionHandler(value = Throwable.class) @ResponseBody public ResponseVo handle(Throwable e, WebRequest request) throws Throwable { if (FeignHolder.getFeign()) { if (request != null && request instanceof ServletWebRequest) { if (e instanceof DefaultException) { logger.warn("Feign path:" + ((ServletWebRequest) request).getRequest().getRequestURL(), e); } else { logger.error("Feign path:" + ((ServletWebRequest) request).getRequest().getRequestURL(), e); } } else { if (e instanceof DefaultException) { logger.warn("Feign error:", e); } else { logger.error("Feign error:", e); } } throw e; } else { if (e instanceof DefaultException) { DefaultException defaultException = (DefaultException) e; if (request != null && request instanceof ServletWebRequest) { logger.warn("ExceptionHandler.handle,{}-{}:{}", ((ServletWebRequest) request).getRequest().getRequestURL(), defaultException.getCode(), defaultException.getMsg()); } else { logger.warn("ExceptionHandler.handle,{}:{}", defaultException.getCode(), defaultException.getMsg()); } return new ResponseVo(defaultException.getCode(), defaultException.getMsg()); } else if (e instanceof ConstraintViolationException) { Iterator<ConstraintViolation<?>> iterator = ((ConstraintViolationException) e).getConstraintViolations().iterator(); String message = iterator.hasNext() ? iterator.next().getMessage() : null; logger.warn("ExceptionHandler.handle,{}-{}:{}", ((ServletWebRequest) request).getRequest().getRequestURL(), ResponseUtil.PARAM_INVALID, message); return ResponseUtil.error(ResponseUtil.PARAM_INVALID, message); } else if (e instanceof MethodArgumentNotValidException) { BindingResult br = ((MethodArgumentNotValidException) e).getBindingResult(); Object model = br.getModel().get(br.getObjectName()); // 如果错误发生在list里,找到具体的model xxx[0].xxx String fieldName = br.getFieldError().getField(); // 有中括号表示是list int indexOf = fieldName.indexOf("["); if (indexOf > 0) { // 截取中括号前的字段名 String actualFieldName = fieldName.substring(0, indexOf); Field actualField = model.getClass().getDeclaredField(actualFieldName); actualField.setAccessible(true); Object list = actualField.get(model); // 得到list中实际发生错误的model model = ((List) list).get(Integer.parseInt(fieldName.substring(indexOf + 1, indexOf + 2))); } String sbLog; if (br.getFieldError().getDefaultMessage() != null) { StringBuilder sb = new StringBuilder(br.getFieldError().getDefaultMessage()); for (Field f : model.getClass().getDeclaredFields()) { ValidKey[] validKeys = f.getAnnotationsByType(ValidKey.class); if (validKeys.length > 0) { ValidKey validKey = validKeys[0]; String keyMessage = validKey.value(); if (keyMessage.contains("{}")) { Field keyField = model.getClass().getDeclaredField(f.getName()); keyField.setAccessible(true); Object keyValue = keyField.get(model); keyMessage = keyMessage.replace("{}", keyValue == null ? "" : String.valueOf(keyValue)); } sb.insert(0, keyMessage); } } sbLog = sb.toString(); } else { ObjectError error = ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors().get(0); sbLog = error.getDefaultMessage(); } logger.warn("ExceptionHandler.handle,{}-{}:{}", ((ServletWebRequest) request).getRequest().getRequestURL(), ResponseUtil.PARAM_INVALID, sbLog); return ResponseUtil.error(ResponseUtil.PARAM_INVALID, sbLog); } else { if (request != null && request instanceof ServletWebRequest) { logger.error("path:" + ((ServletWebRequest) request).getRequest().getRequestURL(), e); } else { logger.error("error:", e); } return new ResponseVo(500, "系统异常"); } } } }
5、针对fegin来说,不希望数据返回的是ResponseVo,希望返回的真实数据。
因为fegin 不会走gateway,所有请求正常的时候不会返回ResponseVo,如果请求出现异常,根据ExceptionHandler,则会返回ResponseVo,因此,在ExceptionHandler中必须要知道请求是否从fegin过来的,因此,我们添加feign请求参数
public class FeignBasicAuthRequestInterceptor implements RequestInterceptor { private final static Logger logger = LoggerFactory.getLogger(FeignBasicAuthRequestInterceptor.class); @Override public void apply(RequestTemplate template) { template.header("isFeign", "1"); } }
public class FeignHolder { private final static Logger logger = LoggerFactory.getLogger(FeignHolder.class); private final static ThreadLocal<Boolean> isFeignHolder = new ThreadLocal<Boolean>(); public static void setIsFeign(String isFeign) { if (StringUtils.isNoneEmpty(isFeign)) { if (isFeign.equals("1")){ isFeignHolder.set(true); } } } public static boolean getFeign() { if (isFeignHolder.get() != null) { return isFeignHolder.get(); } return false; } public static void clear() { isFeignHolder.remove(); } }
在ExceptionHandler 引入判断
@ResponseBody public ResponseVo handle(Throwable e, WebRequest request) throws Throwable { if (FeignHolder.getFeign()) { if (request != null && request instanceof ServletWebRequest) { if (e instanceof DefaultException) { logger.warn("Feign path:" + ((ServletWebRequest) request).getRequest().getRequestURL(), e); } else { logger.error("Feign path:" + ((ServletWebRequest) request).getRequest().getRequestURL(), e); } } else { if (e instanceof DefaultException) { logger.warn("Feign error:", e); } else { logger.error("Feign error:", e); } } throw e; } else { if (e instanceof DefaultException) { DefaultException defaultException = (DefaultException) e; if (request != null && request instanceof ServletWebRequest) { logger.warn("ExceptionHandler.handle,{}-{}:{}", ((ServletWebRequest) request).getRequest().getRequestURL(), defaultException.getCode(), defaultException.getMsg()); } else { logger.warn("ExceptionHandler.handle,{}:{}", defaultException.getCode(), defaultException.getMsg()); } return new ResponseVo(defaultException.getCode(), defaultException.getMsg()); } else if (e instanceof ConstraintViolationException) { Iterator<ConstraintViolation<?>> iterator = ((ConstraintViolationException) e).getConstraintViolations().iterator(); String message = iterator.hasNext() ? iterator.next().getMessage() : null; logger.warn("ExceptionHandler.handle,{}-{}:{}", ((ServletWebRequest) request).getRequest().getRequestURL(), ResponseUtil.PARAM_INVALID, message); return ResponseUtil.error(ResponseUtil.PARAM_INVALID, message); } else if (e instanceof MethodArgumentNotValidException) { BindingResult br = ((MethodArgumentNotValidException) e).getBindingResult(); Object model = br.getModel().get(br.getObjectName()); // 如果错误发生在list里,找到具体的model xxx[0].xxx String fieldName = br.getFieldError().getField(); // 有中括号表示是list int indexOf = fieldName.indexOf("["); if (indexOf > 0) { // 截取中括号前的字段名 String actualFieldName = fieldName.substring(0, indexOf); Field actualField = model.getClass().getDeclaredField(actualFieldName); actualField.setAccessible(true); Object list = actualField.get(model); // 得到list中实际发生错误的model model = ((List) list).get(Integer.parseInt(fieldName.substring(indexOf + 1, indexOf + 2))); } String sbLog; if (br.getFieldError().getDefaultMessage() != null) { StringBuilder sb = new StringBuilder(br.getFieldError().getDefaultMessage()); for (Field f : model.getClass().getDeclaredFields()) { ValidKey[] validKeys = f.getAnnotationsByType(ValidKey.class); if (validKeys.length > 0) { ValidKey validKey = validKeys[0]; String keyMessage = validKey.value(); if (keyMessage.contains("{}")) { Field keyField = model.getClass().getDeclaredField(f.getName()); keyField.setAccessible(true); Object keyValue = keyField.get(model); keyMessage = keyMessage.replace("{}", keyValue == null ? "" : String.valueOf(keyValue)); } sb.insert(0, keyMessage); } } sbLog = sb.toString(); } else { ObjectError error = ((MethodArgumentNotValidException) e).getBindingResult().getAllErrors().get(0); sbLog = error.getDefaultMessage(); } logger.warn("ExceptionHandler.handle,{}-{}:{}", ((ServletWebRequest) request).getRequest().getRequestURL(), ResponseUtil.PARAM_INVALID, sbLog); return ResponseUtil.error(ResponseUtil.PARAM_INVALID, sbLog); } else { if (request != null && request instanceof ServletWebRequest) { logger.error("path:" + ((ServletWebRequest) request).getRequest().getRequestURL(), e); } else { logger.error("error:", e); } return new ResponseVo(500, "系统异常"); } } }
在ErrorDecoder中解析异常
@Configuration public class FeignErrorDecoder implements ErrorDecoder { private final static Logger logger = LoggerFactory.getLogger(FeignErrorDecoder.class); @Override public Exception decode(String methodKey, Response response) { if (response.body() != null) { try { JSONObject exceptionInfo = JSON.parseObject(Util.toString(response.body().asReader())); String exception = exceptionInfo.getString("exception"); if ("DefaultException".equals(exception)) { JSONObject message = JSON.parseObject(exceptionInfo.getString("message")); throw new DefaultException(message.getInteger("code"), message.getString("msg")); } } catch (IOException e) { logger.info("FeignErrorDecoder", e); } } return FeignException.errorStatus(methodKey, response); } }