SpringCloud Gateway从入门到热爱

SpringCloud Gateway从入门到热爱

自从新项目引入Gateway做网关服务以来,感觉一直未平静过,每天都会发生新的问题,特此记录。

1. 入门坑

搭建时,怎么启动都是各种报错,我只是简简单单引入几个包而已,后来发现springcloud 2.x版本Gateway使用的是webFlux,需排除相关spring-boot-starter-web的包,而后便正常启动了!

2. 研究网关predicates、filters配置

各项配置比较完善,感受到了nginx的能力,因为它默认提供了很多过滤器,熟悉的同学根据名字大概就能理解其功能了。

  • predicates通过参数配置实现:
    路径匹配、请求头匹配等等
  • filters通过参数配置实现:
    加请求头、加请求参数、加响应头、
    移除请求头、移除响应头、
    路径前缀、路径截取、重写路径
    重试、
    等等
    SpringCloud Gateway从入门到热爱_第1张图片

3. 各种报错问题解决

  • 以下是因为请求体被订阅消费之后,下次就不能再订阅了。解决方案就是把request重新封装塞回去,网上很多解决方案!
2019-08-15 17:02:41,176|reactor-http-epoll-2|ERROR|o.s.b.a.w.r.error.AbstractErrorWebExceptionHandler|122|[b6e56c5d] 500 Server Error for HTTP POST "/pay-app/app/pointAccount/balance?userId=3"
java.lang.IllegalStateException: Only one connection receive subscriber allowed.
	at reactor.netty.channel.FluxReceive.startReceiver(FluxReceive.java:272)
	at reactor.netty.channel.FluxReceive.subscribe(FluxReceive.java:122)
	at reactor.core.publisher.FluxMap.subscribe(FluxMap.java:62)
	at reactor.netty.ByteBufFlux.subscribe(ByteBufFlux.java:314)
	at reactor.core.publisher.FluxPeek.subscribe(FluxPeek.java:83)
	at reactor.netty.ByteBufFlux.subscribe(ByteBufFlux.java:314)
	at reactor.core.publisher.FluxMap.subscribe(FluxMap.java:62)
	at reactor.core.publisher.FluxMap.subscribe(FluxMap.java:62)
	at reactor.core.publisher.Flux.subscribe(Flux.java:7921)
	at reactor.netty.channel.MonoSendMany.subscribe(MonoSendMany.java:81)
	at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153)
	at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
	at reactor.core.publisher.Mono.subscribe(Mono.java:3848)
	at reactor.netty.NettyOutbound.subscribe(NettyOutbound.java:305)
	at reactor.core.publisher.MonoSource.subscribe(MonoSource.java:51)
	at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
	at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:441)
	at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:470)
	at reactor.netty.resources.PooledConnectionProvider$DisposableAcquire.run(PooledConnectionProvider.java:563)
	at reactor.netty.resources.PooledConnectionProvider$DisposableAcquire.operationComplete(PooledConnectionProvider.java:650)
	at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:502)
	at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:476)
	at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:415)
	at io.netty.util.concurrent.DefaultPromise.addListener(DefaultPromise.java:152)
	at io.netty.util.concurrent.DefaultPromise.addListener(DefaultPromise.java:33)
	at reactor.netty.resources.PooledConnectionProvider.disposableAcquire(PooledConnectionProvider.java:208)
	at reactor.netty.resources.PooledConnectionProvider.lambda$acquire$2(PooledConnectionProvider.java:162)
	at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:57)
	at reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.lambda$subscribe$0(HttpClientConnect.java:329)
	at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:57)
	at reactor.core.publisher.Mono.subscribe(Mono.java:3848)
  • 以下是因为responseheader里的content-type是null造成的,响应头一定要有content-type:
2019-08-16 16:23:04,109|reactor-http-client-epoll-9|ERROR|o.s.b.a.w.r.error.DefaultErrorWebExceptionHandler|218|Failed to handle request [GET http://192.168.200.51:8080/aggregation-app/webjars/springfox-swagger-ui/swagger-ui-bundle.js?v=2.9.2]
java.lang.NullPointerException: null
	at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
	at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
	at org.springframework.cloud.gateway.filter.NettyRoutingFilter.lambda$filter$3(NettyRoutingFilter.java:117)
	at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:177)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:108)
	at reactor.core.publisher.FluxRetryPredicate$RetryPredicateSubscriber.onNext(FluxRetryPredicate.java:81)
	at reactor.core.publisher.MonoCreate$DefaultMonoSink.success(MonoCreate.java:146)
	at reactor.ipc.netty.channel.PooledClientContextHandler.fireContextActive(PooledClientContextHandler.java:87)
	at reactor.ipc.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:584)
	at reactor.ipc.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:138)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340)
	at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:438)
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:310)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:284)
	at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348)

以下报错是因为requestHeader中缺少content-type字段导致的:

2019-08-21 16:41:36,895|reactor-http-server-epoll-7|ERROR|o.s.b.a.w.r.error.DefaultErrorWebExceptionHandler|218|Failed to handle request [POST http://192.168.200.51:8080/aggregation-app/admin/systemNotice/listPage?pageNum=1&pageSize=10]
java.lang.UnsupportedOperationException: null
 at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
 at org.springframework.http.HttpHeaders.set(HttpHeaders.java:1456)
 at org.springframework.http.HttpHeaders.setContentType(HttpHeaders.java:854)
 at org.springframework.http.codec.EncoderHttpMessageWriter.updateContentType(EncoderHttpMessageWriter.java:133)
 at org.springframework.http.codec.EncoderHttpMessageWriter.write(EncoderHttpMessageWriter.java:102)
 at org.springframework.web.reactive.function.BodyInserters.lambda$null$9(BodyInserters.java:322)
 at java.util.Optional.map(Optional.java:215)
 at org.springframework.web.reactive.function.BodyInserters.lambda$bodyInserterFor$12(BodyInserters.java:314)
 at com.dzjk.innovation.gateway.filter.JwtCheck2GatewayFilterFactory.just2(JwtCheck2GatewayFilterFactory.java:152)
 at com.dzjk.innovation.gateway.filter.JwtCheck2GatewayFilterFactory.lambda$apply$0(JwtCheck2GatewayFilterFactory.java:120)
 at org.springframework.cloud.gateway.filter.OrderedGatewayFilter.filter(OrderedGatewayFilter.java:44)
 at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain.lambda$filter$0(FilteringWebHandler.java:115)
 at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:45)

//  解决办法:exchange取出来的headers是固定长度不支持操作,new一个新的放进去
HttpHeaders headers = new HttpHeaders();
headers.addAll(exchange.getRequest().getHeaders());
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
  • 以下报错是因为请求体被截断后,json解析报错
019-08-21 11:28:25,013|reactor-http-server-epoll-8|ERROR|o.s.b.a.w.r.error.DefaultErrorWebExceptionHandler|218|Failed to handle request [POST http://adminweb-dev.csfl.com/aggregation-app/admin/platformInfo/save]
com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unterminated string at line 1 column 295 path $.financingRecord
 at com.google.gson.internal.Streams.parse(Streams.java:60)
 at com.google.gson.JsonParser.parse(JsonParser.java:84)
 at com.google.gson.JsonParser.parse(JsonParser.java:59)
 at com.google.gson.JsonParser.parse(JsonParser.java:45)
 at com.dzjk.innovation.gateway.filter.JwtCheckGatewayFilterFactory.rebuildBodyStr(JwtCheckGatewayFilterFactory.java:239)
 at com.dzjk.innovation.gateway.filter.JwtCheckGatewayFilterFactory.addUserId(JwtCheckGatewayFilterFactory.java:173)
 at com.dzjk.innovation.gateway.filter.JwtCheckGatewayFilterFactory.lambda$apply$0(JwtCheckGatewayFilterFactory.java:110)
 at org.springframework.cloud.gateway.filter.OrderedGatewayFilter.filter(OrderedGatewayFilter.java:44)
 at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain.lambda$filter$0(FilteringWebHandler.java:115)
 at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:45)

关于参数被截断丢失的问题,最终还是参照了org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory 此类的写法,最终解决问题。
起先我也是参照网上gateway修改请求体的写法,是因为没看懂 ModifyRequestBodyGatewayFilterFactory这类,后来又仔细学习了一下,其实核心就是 ModifyRequestBodyGatewayFilterFactory.Config 类,其他的逻辑都会处理好。
看不懂这个类的主要原因还是对webflux不了解。

public static class Config {
		private Class inClass = String.class;	// 入参解析为此对象
		private Class outClass = String.class;	// 出参数解析为此对象
		private MediaType mediaType;
        private GetUserId userId;
		@Deprecated
		private Map inHints;	// 我没用到
		@Deprecated
		private Map outHints;	// 我没用到

		// 重写函数,修改请求体就是通过此实现类进行重写
		private RewriteFunction rewriteFunction = new RewriteFunction() {
            @Override
            public Publisher apply(ServerWebExchange exchange, String bodyStr) {
            	// 修改请求体逻辑
                String res = rebuildBodyStr(bodyStr, contentType, userId);
                return Mono.just(res);
            }
        };
		
}
  • 空请求体时,参数修改逻辑未生效,通过debug定位到代码reactor.core.publisher.Operators.MonoSubscriber#complete
public final void complete(O v) {
			int state = this.state;
			for (; ; ) {
				if (state == FUSED_EMPTY) {
					setValue(v);
					STATE.lazySet(this, FUSED_READY);

					Subscriber a = actual;
					a.onNext(v);
					if (this.state != CANCELLED) {
						a.onComplete();
					}
					return;
				}

//  有请求没值,直接了,故逻辑直接跳过了
				// if state is >= HAS_CANCELLED or bit zero is set (*_HAS_VALUE) case, return
				if ((state & ~HAS_REQUEST_NO_VALUE) != 0) {
					return;
				}

				if (state == HAS_REQUEST_NO_VALUE) {
					STATE.lazySet(this, HAS_REQUEST_HAS_VALUE);
					Subscriber a = actual;
					a.onNext(v);
					if (this.state != CANCELLED) {
						a.onComplete();
					}
					return;
				}
				setValue(v);
				if (STATE.compareAndSet(this, NO_REQUEST_NO_VALUE, NO_REQUEST_HAS_VALUE)) {
					return;
				}
				state = this.state;
				if (state == CANCELLED) {
					value = null;
					return;
				}
			}
		}
  • 请求出现问题,前端请求体是application/json,可到了后端日志变成了:application/x-www-form-urlencoded,因为代码逻辑根据 Content-Type 处理,造成了BUG,通过打印日志并没有发现问题,后仔细思考后想了想应该是共享变量对问题,便对自己创建对对象和com.dzjk.innovation.gateway.filter.JwtCheck2GatewayFilterFactory.Config打印hashcode,发现任何请求【Config】这类对hashcode都是一样的,说明此对象是所有线程共享对,而我往对象里面存了属性值。
  1. 那么并发情况下,比如A线程设值,接着B线程设值,而A线程取值使用,取出来的是B线程设定的值,造成数据错乱,引发BUG!
  2. 使用ThreadLocal解决了此问题。
  3. 请看下图,因为config是共享对象,这样设置属性便是BUG。SpringCloud Gateway从入门到热爱_第2张图片

你可能感兴趣的:(springcloud)