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);
    }
}

 

posted @ 2019-07-09 15:58  微笑不加冰  阅读(7004)  评论(0编辑  收藏  举报