SpringCloud Gateway 修改响应体(统一前端响应格式)

 

在全网找了好几天,又去springcloud的issue中翻阅,也没有真正说该怎么写的,都是各种方式操作流,要么就是说参考ModifyRequestBodyGatewayFilterFactory。。

 

======================

2020年4月13日22:17:49

虽然可以统一响应格式,但是我仔细思考后想到,已经有了HTTP状态码,前端或者客户端本身已经可以根据状态码进行判断处理,而一些校验性判断可使用 hibernate-validator,其他业务性 例如异常可自定义一个业务异常,抛出业务异常并使用HTTP状态码,像一些例如:空指针、角标越界完全可以在代码中避免 应在开发阶段避免,不会再测试或生产中出现,现在出现一个问题是,例如使用 RestTemplate 调用第三方服务时,第三方会根据不同的厂家返回不同的码值,而不管是什么 如果抛出异常就是RestClientException,返回前端时就会是500 嵌套异常,此时应将最内层嵌套分解出来,并返回前端。

仅个人想法

======================

 

版本:

springboot 2.0.6

springcloud Finchley.SR2

hutool 4.4.2

lombok

主要参考:ModifyRequestBodyGatewayFilterFactory 这个类

package com.gitee.cashzhang27.ftf.common.core.entity;


import com.gitee.cashzhang27.ftf.common.core.constants.enums.ErrorCode;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @author Cash Zhang
 * @version v1.0
 * @since 2019/01/28 21:47
 */
@Data
@Accessors(chain = true)
public class Response<T> implements Serializable {

  private static final long serialVersionUID = 5005318709330004756L;
  /**
   * 状态码
   */
  private Integer code;
  /**
   * 描述
   */
  private String msg;
  /**
   * 结果集
   */
  private T data;

  public static <T> Response<T> ok() {
    return restResult(ErrorCode.SUCCESS, null);
  }

  public static <T> Response<T> ok(T data) {
    return restResult(ErrorCode.SUCCESS, data);
  }

  public static <T> Response<T> ok(ICode iCode, T data) {
    return restResult(iCode, data);
  }

  public static <T> Response<T> failed(ICode iCode) {
    return restResult(iCode, null);
  }

  public static <T> Response<T> failed(int code, String msg) {
    return restResult(code, msg, null);
  }

  private static <T> Response<T> restResult(ICode iCode, T data) {
    return restResult(iCode.getCode(), iCode.getMsg(), data);
  }

  private static <T> Response<T> restResult(Integer code, String msg, T data) {
    Response<T> apiResult = new Response<>();
    apiResult.setCode(code);
    apiResult.setMsg(msg);
    apiResult.setData(data);
    return apiResult;
  }

}
package com.gitee.cashzhang27.ftf.common.core.entity;

/**
 * 用于返回状态码及描述
 *
 * @author Cash Zhang
 * @version v1.0
 * @since 2019/01/28 21:48
 */
public interface ICode {

  /**
   * 获取状态码
   *
   * @return 状态码
   */
  Integer getCode();

  /**
   * 获取描述
   *
   * @return 描述
   */
  String getMsg();
}
package com.gitee.cashzhang27.ftf.gateway.filter;

import static com.gitee.cashzhang27.ftf.gateway.configuration.SwaggerProvider.API_URI;
import static org.springframework.cloud.gateway.filter.NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR;

import cn.hutool.json.JSONUtil;
import com.gitee.cashzhang27.ftf.common.core.entity.Response;
import org.apache.commons.lang.StringEscapeUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.DefaultClientResponse;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * @author Cash Zhang
 * @version v1.0
 * @since 2019/03/16 19:20
 */
@Component
public class ResponseGlobalFilter implements GlobalFilter, Ordered {

  private static final String SUCCESS_PREFIX = "{\"code\":20000,\"msg\":\"success\",\"data\":";
  private static final String SUCCESS_SUFFIX = "}";

  @Override
  public int getOrder() {
    // -1 is response write filter, must be called before that
    return WRITE_RESPONSE_FILTER_ORDER - 1;
  }

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpResponse originalResponse = exchange.getResponse();
    ServerHttpRequest request = exchange.getRequest();
    ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(
        exchange.getResponse()) {

      @Override
      @SuppressWarnings("unchecked")
      public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) {

        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(String.class).flatMap(originalBody -> {
          if (originalResponse.getStatusCode() != null) {
            if (originalResponse.getStatusCode().is2xxSuccessful()) {
              // Swagger 请求不做包装
              // 自定义 Response 实体不做包装
              if (API_URI.equals(request.getURI().getPath()) || originalBody
                  .startsWith(StringEscapeUtils.unescapeJavaScript(SUCCESS_PREFIX))) {
                return Mono.just(originalBody);
              }
              if (!JSONUtil.isJson(originalBody)) {
                originalBody = JSONUtil.toJsonStr(Response.ok(originalBody));
              } else {
                originalBody = StringEscapeUtils.unescapeJavaScript(SUCCESS_PREFIX) + originalBody
                    + SUCCESS_SUFFIX;
              }
            } else {
              originalBody = JSONUtil.toJsonStr(originalBody);
            }
          }
          return Mono.just(originalBody);
        });

        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange,
            exchange.getResponse().getHeaders());
        return bodyInserter.insert(outputMessage, new BodyInserterContext())
            .then(Mono.defer(() -> {
              long contentLength1 = getDelegate().getHeaders().getContentLength();
              Flux<DataBuffer> messageBody = outputMessage.getBody();
              //TODO: if (inputStream instanceof Mono) {
              HttpHeaders headers = getDelegate().getHeaders();
              if (/*headers.getContentLength() < 0 &&*/ !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(@NonNull
          Publisher<? extends Publisher<? extends DataBuffer>> body) {
        return writeWith(Flux.from(body)
            .flatMapSequential(p -> p));
      }
    };

    return chain.filter(exchange.mutate().response(responseDecorator).build());
  }


  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<DataBuffer>) body;
      } else {
        flux = ((Mono) body).flux();
      }
    }

    @Override
    public @NonNull
    Flux<DataBuffer> getBody() {
      return flux;
    }

    @Override
    public @NonNull
    HttpHeaders getHeaders() {
      return headers;
    }

    @Override
    public @NonNull
    HttpStatus getStatusCode() {
      return null;
    }

    @Override
    public int getRawStatusCode() {
      return 0;
    }

    @Override
    public @NonNull
    MultiValueMap<String, ResponseCookie> getCookies() {
      return null;
    }
  }

}
package com.gitee.cashzhang27.ftf.common.security.exception;

import com.gitee.cashzhang27.ftf.common.core.constants.enums.ErrorCode;
import com.gitee.cashzhang27.ftf.common.core.entity.Response;
import com.gitee.cashzhang27.ftf.common.core.exception.StrategyException;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 *
 * @author Cash Zhang
 * @version v1.0
 * @since 2019/01/31 20:29
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

  /**
   * 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
   *
   * @param binder 绑定器
   */
  @InitBinder
  public void initBinder(WebDataBinder binder) {
  }

  /**
   * GlobalException.
   *
   * @param e Exception
   * @return Response
   */
  @ExceptionHandler(Exception.class)
  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  public Response handleGlobalException(Exception e) {
    log.error("全局异常 ex={}", e.getMessage(), e);
    return Response.failed(ErrorCode.GLOBAL_EXCEPTION.getCode(), e.getMessage());
  }

  /**
   * MethodArgumentNotValidException
   *
   * @param e MethodArgumentNotValidException
   * @return Response
   */
  @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public Response handleBodyValidException(MethodArgumentNotValidException e) {
    List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
    log.error("参数校验异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
    return Response.failed(ErrorCode.VALIDATION_EXCEPTION.getCode(),
        fieldErrors.get(0).getDefaultMessage());
  }

  /**
   * BadCredentialsException.
   *
   * @param e IllegalArgumentException
   * @return Response
   */
  @ExceptionHandler(BadCredentialsException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public Response handleArgumentException(BadCredentialsException e) {
    log.error("凭证错误异常 ex={}", e.getMessage(), e);
    return Response.failed(ErrorCode.BAD_CREDENTIALS.getCode(), e.getMessage());
  }

}

 

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值