-
前言
在如今的服务化开发过程中,特别是各服务模块开发之间调用,难免出现异常甚至报错的情况,为了给下游服务更友好的提示,通常选择在网关处理全局异常以及封装统一响应体,这样就避免了在每个模块都写重复的代码。在多模块开发过程中,通常使用公共服务jar包引入的规则,定义好一些边界问题,那么本文主要介绍网关如何处理全局异常以及封装响应体返回给下游或者视图层。
-
准备
笔者日常开发采用springCloud alibaba 这套框架组件,主要用nacos做注册与配置中心,springBoot为项目主体工程。
所以不管咋样,你能看到我这里,说明平常你可能也是用的这套框架,那么,恭喜,下面将介绍如何使用GateWay处理网关异常。
-
思路
了解网关基本知识很重要的,起码你要知道异常信息肯定是要拦截并处理的,从应用层抛出异常到网关,网关进行异常信息的拦截,同时网关解析相应错误码,封装成json响应体返回。大体是这么个思路。
-
GateWay过滤器介绍
Spring Cloud GateWay除了具备请求路由功能之外,也支持对请求的过滤。过滤器的生命周期就不说了把,跟以前那个serlvet类似,这里主要介绍两种过滤器类型:一种是GatewayFilter、另一种是GlobalFilter;前者应用到单个路由或者一个分组的路由上,后者应用到所有的路由上。
既然全局过滤器作用于所有的路由,Spring Cloud Gateway定义了Global Filter接口,用户可以自定义实现自己的GlobalFilter。通过全局过滤器可以实现对异常的处理。
-
上代码
- 自定义一个接口ComplexFilter.class、接口实现GatewayFilter、GlobalFilter、Ordered
public interface ComplexFilter extends GatewayFilter, GlobalFilter, Ordered { }
-
接着自定义一个过滤器JsonResponseWrapperFilter.class、实现ComplexFilter、重写filter方法以及针对该过滤器排个序
排序值越小,过滤器优先级越高。
@Component @Slf4j public class JsonResponseWrapperFilter implements ComplexFilter { public static final String IS_IGNOREAUTHFILTER = "ingore"; @Override public int getOrder() { return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //操作针对某些路由跳过全局过滤器 if(exchange.getAttributes().get(IS_IGNOREAUTHFILTER)!=null){ return chain.filter(exchange); } //包装响应体 ServerWebExchange newExchange = exchange.mutate().response( ServerHttpResponseDecoratorHelper.build(exchange, (originalBody) -> { String requestUri = exchange.getRequest().getPath().pathWithinApplication().value(); MediaType responseMediaType = exchange.getResponse().getHeaders().getContentType(); log.info("Request [{}] response content-type is {}", requestUri, responseMediaType); if (MediaType.APPLICATION_JSON.isCompatibleWith(responseMediaType)) { return rewriteBody(exchange, originalBody); } else { return Mono.just(originalBody); } })).build(); return chain.filter(newExchange); }
- 定义ServerHttpResponseDecoratorHelper.class 工具类消除模板代码,在读取或修改response body时必须用到ServerHttpResponseDecorator
import org.reactivestreams.Publisher; import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage; import org.springframework.cloud.gateway.support.BodyInserterContext; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.function.Function; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR; /** * ServerHttpResponseDecoratorHelper工具类消除模板代码 * 在读取或修改response body时必须用到ServerHttpResponseDecorator * * @author TT */ public class ServerHttpResponseDecoratorHelper { public static ServerHttpResponseDecorator build(ServerWebExchange exchange, MyRewriteFunction<byte[], byte[]> rewriteFunction) { return new ServerHttpResponseDecorator(exchange.getResponse()) { @Override public Mono<Void> writeWith(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); ClientResponse clientResponse = prepareClientResponse(body, httpHeaders); if (!MediaType.APPLICATION_JSON.isCompatibleWith(httpHeaders.getContentType())) { return super.writeWith(body); } // TODO: flux or mono Mono<byte[]> modifiedBody = clientResponse.bodyToMono(byte[].class) .flatMap(originalBody -> rewriteFunction.apply(originalBody)); return bodyInsert(this, exchange, modifiedBody); } @Override public Mono<Void> writeAndFlushWith( Publisher<? extends Publisher<? extends DataBuffer>> body) { return writeWith(Flux.from(body).flatMapSequential(p -> p)); } private ClientResponse prepareClientResponse( Publisher<? extends DataBuffer> body, HttpHeaders httpHeaders) { ClientResponse.Builder builder; builder = ClientResponse .create(exchange.getResponse().getStatusCode()); return builder.headers(headers -> headers.putAll(httpHeaders)) .body(Flux.from(body)).build(); } }; } public static Mono<Void> bodyInsert(ServerHttpResponseDecorator decorator, ServerWebExchange exchange, Mono<byte[]> modifiedBody) { BodyInserter<Mono<byte[]>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, byte[].class); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage( exchange, exchange.getResponse().getHeaders()); return bodyInserter.insert(outputMessage, new BodyInserterContext()) .then(Mono.defer(() -> { Flux<DataBuffer> messageBody = outputMessage.getBody(); HttpHeaders headers = decorator.getDelegate().getHeaders(); if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { messageBody = messageBody.doOnNext(data -> headers .setContentLength(data.readableByteCount())); } // TODO: fail if isStreamingMediaType? return decorator.getDelegate().writeWith(messageBody); })); } public interface MyRewriteFunction<T, R> extends Function<T, Mono<R>> { } }
可以看到以上代码:geOrder()方法决定过滤器优先级,filter方法对过来的请求进行拦截处理。这里在里面对请求进行了识别,接受json/application格式,同时对响应返回体进行了封装。具体看如下是怎么封装的代码。
private Mono<byte[]> rewriteBody(ServerWebExchange exchange, byte[] originalBody) { HttpStatus originalResponseStatus = exchange.getResponse().getStatusCode(); //将状态码统一重置为200,在这里重置才是终极解决办法 log.debug("Response status code is {} , body is {}", originalResponseStatus, new String(originalBody)); if (originalResponseStatus == HttpStatus.OK) { exchange.getResponse().setStatusCode(HttpStatus.OK); if (originalBody == null) { log.debug("下游服务响应内容为空,但是http状态码为200,则按照成功的响应体包装返回"); return makeMono(R.ok()); } else { try { //只能parse出JSONObject、JSONArray、Integer、Boolean等类型,当是一个string但是非json格式则抛出异常 Object jsonObject = JSON.parse(originalBody); //如果响应内容已经包含了errcode字段,则表示下游的响应体本身已经是统一结果体了,无需再包装 if ((jsonObject instanceof JSONObject) && ((JSONObject) jsonObject).containsKey("errcode")) { log.debug("服务响应体已经是统一结果体,无需包装"); return Mono.just(originalBody); } else { return makeMono(R.ok(jsonObject)); } } catch (Exception e) { log.error("解析下游响应体异常", e); return makeMono(R.ok(originalBody)); } } } else { //如果不是401和403异常则重置为200状态码 if (!ArrayUtils.contains(new int[]{401, 403}, originalResponseStatus.value())) { exchange.getResponse().setStatusCode(HttpStatus.OK); } //响应异常的报文 if (originalBody == null) { return Mono.just(JSON.toJSONBytes(R.failed( ErrorCode.GATEWAY_DOWNSTREAM_NO_PROVIDE_ERROR.getErrorCode(), ErrorCode.GATEWAY_DOWNSTREAM_NO_PROVIDE_ERROR.getDescription() ), SerializerFeature.WriteMapNullValue)); } else { try { //只能parse出JSONObject、JSONArray、Integer、Boolean等类型,当是一个string但是非json格式则抛出异常 Object jsonObject = JSON.parse(originalBody); //如果响应内容已经包含了errcode字段,则表示下游的响应体本身已经是统一结果体了 if ((jsonObject instanceof JSONObject)) { JSONObject jo = ((JSONObject) jsonObject); if (jo.containsKey("errcode")) { return Mono.just(originalBody); } else if (jo.containsKey("status") && jo.containsKey("errorCode")) { int errorCode = jo.getIntValue("errorCode"); String message = jo.getString("message"); return Mono.just(JSON.toJSONBytes(R.failed(errorCode, message) , SerializerFeature.WriteMapNullValue)); } else if ("404".equals(jo.getString("status"))) { //下游返回了404 return Mono.just(JSON.toJSONBytes(R.failed( ErrorCode.GATEWAY_DOWNSTREAM_RESOURCE_NOT_FOUND.getErrorCode(), ErrorCode.GATEWAY_DOWNSTREAM_RESOURCE_NOT_FOUND.getDescription() ), SerializerFeature.WriteMapNullValue)); } else if ("405".equals(jo.getString("status"))) { //下游返回了405 return Mono.just(JSON.toJSONBytes(R.failed( ErrorCode.METHOD_NOT_ALLOWED.getErrorCode(), ErrorCode.METHOD_NOT_ALLOWED.getDescription() ), SerializerFeature.WriteMapNullValue)); } else if ("415".equals(jo.getString("status"))) { //下游返回了415 return Mono.just(JSON.toJSONBytes(R.failed( ErrorCode.UNSUPPORTED_MEDIA_TYPE.getErrorCode(), ErrorCode.UNSUPPORTED_MEDIA_TYPE.getDescription() ))); } else { //下游返回的包体是一个jsonobject,并不是规范的错误包体 return Mono.just(JSON.toJSONBytes(R.failed( ErrorCode.GATEWAY_DOWNSTREAM_ERROR_INFO_FORMAT_ERROR.getErrorCode(), ErrorCode.GATEWAY_DOWNSTREAM_ERROR_INFO_FORMAT_ERROR.getDescription(), originalBody ), SerializerFeature.WriteMapNullValue)); } } else { //不是一个jsonobject,可能是一个jsonarray return Mono.just(JSON.toJSONBytes(R.failed( ErrorCode.GATEWAY_DOWNSTREAM_ERROR_INFO_FORMAT_ERROR.getErrorCode(), ErrorCode.GATEWAY_DOWNSTREAM_ERROR_INFO_FORMAT_ERROR.getDescription(), originalBody ), SerializerFeature.WriteMapNullValue)); } } catch (Exception e) { log.error("解析下游响应体异常", e); return Mono.just(JSON.toJSONBytes(R.failed( ErrorCode.GATEWAY_DOWNSTREAM_ERROR_INFO_FORMAT_ERROR.getErrorCode(), ErrorCode.GATEWAY_DOWNSTREAM_ERROR_INFO_FORMAT_ERROR.getDescription(), originalBody ), SerializerFeature.WriteMapNullValue)); } } } }
private Mono<byte[]> makeMono(R<?> result) { return Mono.just(JSON.toJSONBytes(result, SerializerFeature.WriteMapNullValue)); }
至此过滤器定义完毕,有时候为了过滤某些路由不走全局过滤器,通常我们重新定义一个过滤器且这个过滤器的优先级大于全局过滤器。结合上面代码请看以下定义的忽略部分路由的过滤器代码。
-
忽略某些路由跳过全局过滤器
package com.tungee.crm.config; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 设置忽略路由 * @author TT */ @Component @Slf4j public class IgnoreAuthFilter extends AbstractGatewayFilterFactory<IgnoreAuthFilter.Config> { public IgnoreAuthFilter() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return new InnerFilter(config); } /** * 创建一个内部类,来实现2个接口,指定顺序 * 这里通过Ordered指定优先级 */ private class InnerFilter implements GatewayFilter, Ordered { private Config config; InnerFilter(Config config) { this.config = config; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (config.isIgnoreGlobalFilter() == true) { exchange.getAttributes().put(JsonResponseWrapperFilter.IS_IGNOREAUTHFILTER, true); } return chain.filter(exchange); } //这里优先级要大于全局过滤器 @Override public int getOrder() { return -1000; } } public static class Config{ boolean ignoreGlobalFilter; public boolean isIgnoreGlobalFilter() { return ignoreGlobalFilter; } public void setIgnoreGlobalFilter(boolean ignoreGlobalFilter) { this.ignoreGlobalFilter = ignoreGlobalFilter; } } /** *这个name方法 用来在yml配置中指定对应的过滤器名称 **/ @Override public String name() { return "IgnoreAuthFilter"; } }
-
gateway.yaml配置
spring: cloud: gateway: routes: - id: gateway-demo uri: lb://gateway-demo order: 20 predicates: - Path=/api/crm/message/** filters: - StripPrefix=3 - name: IgnoreAuthFilter #本路由跳过全局过滤器 args: ignoreGlobalFilter: true
-
postman请求正常结果
{ "data": { "companyId": "5c334fc3931dac2894f3ab02", "departmentId": "5d731760931dac19195fd7f9", "templateId": "5f6aadea2b2dd2614c45e4e2", "cycleDate": 30 }, "errcode": 0, "errmsg": "ok" }
-
postman请求异常响应
{ "data": "eyJ0aW1lc3RhbXAiOiIyMDIwLTEyLTE1Iiwic3RhdHVzIjo1MDAsImVycm9yIjoiSW50ZXJuYWwgU2VydmVyIEVycm9yIiwibWVzc2FnZSI6IuWvueixoeaIluWxnuaAp+S4jeWtmOWcqCIsInBhdGgiOiIvdGVtcGxhdGVTZXR0aW5nUmVjb3JkL2dldFRlbXBsYXRlQnlDb21wYW55SWQifQ==", "errcode": 41004, "errmsg": "下游错误信息格式不规范" }