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);
}
}
来源:oschina
链接:https://my.oschina.net/u/4313718/blog/3471187