一、官方推荐的方式
参照:ModifyRequestBodyGatewayFilterFactory、ModifyResponseBodyGatewayFilterFactory
特点:实现简单,但测试发现并发吞吐量上不去,在8c16G的机器,吞吐量只有不到300/s,rt过长(>500ms)
代码实现:
请求体打印 |
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import com.dtyunxi.util.DateUtil; import com.dtyunxi.yundt.cube.center.gateway.service.RouterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.BodyInserterContext; import org.springframework.cloud.gateway.support.CachedBodyOutputMessage; import org.springframework.cloud.gateway.support.DefaultServerRequest; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; 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.server.HandlerStrategies; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono;
import java.util.HashMap; import java.util.List; import java.util.function.Function;
/** * @author yue.na * @date 2019/6/18 * @since 2.0.0 */ @Component @ConditionalOnProperty(name = "support-datagram-record", havingValue = "true") public class RequestRecordFilter implements GlobalFilter, Ordered { Class inClass = String.class; Class outClass = String.class; private final List private static Logger logger = LoggerFactory.getLogger(RequestRecordFilter.class);
@Autowired private RouterService routerService;
@Override public Mono // 请求报文打印 String requestId = ""; Function requestRead = body -> { // 这个body就是请求体 return Mono.just(body); }; ServerRequest serverRequest = new DefaultServerRequest(exchange, this.messageReaders); Mono> modifiedBody = serverRequest.bodyToMono(inClass).flatMap(requestRead); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass); HttpHeaders headers = new HttpHeaders(); headers.putAll(exchange.getRequest().getHeaders()); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public HttpHeaders getHeaders() { long contentLength = headers.getContentLength(); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); if (contentLength > 0L) { httpHeaders.setContentLength(contentLength); } else { httpHeaders.set("Transfer-Encoding", "chunked"); }
return httpHeaders; }
@Override public Flux return outputMessage.getBody(); } }; return chain.filter(exchange.mutate().request(decorator).build()); })); }
@Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }
|
响应体打印 |
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import com.dtyunxi.util.DateUtil; import com.dtyunxi.yundt.cube.center.gateway.service.RouterService; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.BodyInserterContext; import org.springframework.cloud.gateway.support.CachedBodyOutputMessage; import org.springframework.cloud.gateway.support.DefaultClientResponse; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.Ordered; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; 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.codec.ClientCodecConfigurer; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; 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;
import java.util.HashMap; import java.util.function.Consumer; import java.util.function.Function;
/** * @author yue.na * @date 2019/6/18 * @since 2.0.0 */ @Component @ConditionalOnProperty(name = "support-datagram-record", havingValue = "true") public class ReponseRecordFilter implements GlobalFilter, Ordered {
Class inClass = String.class; Class outClass = String.class; private static Logger logger = LoggerFactory.getLogger(ReponseRecordFilter.class);
private static Decoder decoder = new Jackson2JsonDecoder(); private static Encoder encoder = new Jackson2JsonEncoder();
@Autowired private RouterService routerService;
private static final ExchangeStrategies STRATEGIES;
static { STRATEGIES = ExchangeStrategies.builder().codecs(new Consumer @Override public void accept(ClientCodecConfigurer clientCodecConfigurer) { ClientCodecConfigurer.ClientDefaultCodecs clientDefaultCodecs = clientCodecConfigurer.defaultCodecs();
clientDefaultCodecs.jackson2JsonDecoder(decoder); clientDefaultCodecs.jackson2JsonEncoder(encoder); } }).build(); }
@Override public Mono // 响应报文打印 String requestId = ""; Function responseRead = body -> { // 这个body就是请求体 return Mono.just(body); }; ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) { @Override public Mono HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Content-Type", "application/json"); ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders); // DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults()); DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, STRATEGIES); Mono modifiedBody = clientResponse.bodyToMono(inClass).flatMap(responseRead); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders()); return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { Flux HttpHeaders headers = this.getDelegate().getHeaders(); if (!headers.containsKey("Transfer-Encoding")) { messageBody = messageBody.doOnNext((data) -> { headers.setContentLength((long) data.readableByteCount()); }); } return this.getDelegate().writeWith(messageBody); })); }
@Override public Mono return this.writeWith(Flux.from(body).flatMapSequential((p) -> { return p; })); } }; return chain.filter(exchange.mutate().response(responseDecorator).build()); }
@Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
public static class ResponseAdapter implements ClientHttpResponse { private final Flux private final HttpHeaders headers;
public ResponseAdapter(Publisher extends DataBuffer> body, HttpHeaders headers) { this.headers = headers; if (body instanceof Flux) { this.flux = (Flux) body; } else { this.flux = ((Mono) body).flux(); }
}
@Override public Flux return this.flux; }
@Override public HttpHeaders getHeaders() { return this.headers; }
@Override public HttpStatus getStatusCode() { return null; }
@Override public int getRawStatusCode() { return 0; }
@Override public MultiValueMap return null; } } }
|
二、从Flux
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) { //获取请求体 Flux
AtomicReference body.subscribe(buffer -> { CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()); DataBufferUtils.release(buffer); bodyRef.set(charBuffer.toString()); }); //获取request body return bodyRef.get(); } |
详细实现参考:https://my.oschina.net/zcqshine/blog/2875060
特点:请求体会被截断,为了解决截断问题需要额外配置。但并发吞吐量高与上面一样的配置下,能达到5000/s,RT(100ms~)。
如果你发现了请求体获取不完整,被截断,可以试试重写下面那个类的实现
重写后的HttpServer (看TODO部分) |
/* * Copyright (c) 2011-2019 Pivotal Software Inc, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
package reactor.ipc.netty.http.server;
import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.logging.LoggingHandler; import io.netty.util.NetUtil; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; import reactor.ipc.netty.*; import reactor.ipc.netty.channel.ContextHandler; import reactor.ipc.netty.http.HttpResources; import reactor.ipc.netty.options.ServerOptions; import reactor.ipc.netty.tcp.BlockingNettyContext; import reactor.ipc.netty.tcp.TcpServer;
import java.net.InetSocketAddress; import java.util.Objects; import java.util.Properties; import java.util.function.*;
/** * Base functionality needed by all servers that communicate with clients over HTTP. * * @author Stephane Maldini * @author Violeta Georgieva */ public final class HttpServer implements NettyConnector
/** * Build a simple Netty HTTP server listening on localhost (127.0.0.1) and * port {@literal 8080}. * * @return a simple HTTP Server */ public static HttpServer create() { return builder().build(); }
/** * Build a simple Netty HTTP server listening over bind address and port passed * through the {@link HttpServerOptions}. * Use {@literal 0} to let the system assign a random port. * * @param options the options for the server, including bind address and port. * @return a simple HTTP server */ public static HttpServer create(Consumer super HttpServerOptions.Builder> options) { return builder().options(options).build(); }
/** * Build a simple Netty HTTP server listening on localhost (127.0.0.1) and the provided * port * Use {@literal 0} to let the system assign a random port. * * @param port the port to listen to, or 0 to dynamically attribute one. * @return a simple HTTP server */ public static HttpServer create(int port) { return builder().listenAddress(new InetSocketAddress(port)).build(); }
/** * Build a simple Netty HTTP server listening on the provided bind address and * port {@literal 8080}. * * @param bindAddress address to listen for (e.g. 0.0.0.0 or 127.0.0.1) * @return a simple HTTP server */ public static HttpServer create(String bindAddress) { return builder().bindAddress(bindAddress).build(); }
/** * Build a simple Netty HTTP server listening on the provided bind address and port. * * @param bindAddress address to listen for (e.g. 0.0.0.0 or 127.0.0.1) * @param port the port to listen to, or 0 to dynamically attribute one. * @return a simple HTTP server */ public static HttpServer create(String bindAddress, int port) { return builder().bindAddress(bindAddress).port(port).build(); }
/** * Creates a builder for {@link HttpServer HttpServer} * * @return a new HttpServer builder */ public static HttpServer.Builder builder() { return new HttpServer.Builder(); }
private final TcpBridgeServer server; final HttpServerOptions options;
private HttpServer(HttpServer.Builder builder) { HttpServerOptions.Builder serverOptionsBuilder = HttpServerOptions.builder(); if (Objects.isNull(builder.options)) { if (Objects.isNull(builder.bindAddress)) { serverOptionsBuilder.listenAddress(builder.listenAddress.get()); } else { serverOptionsBuilder.host(builder.bindAddress).port(builder.port); } } else { builder.options.accept(serverOptionsBuilder); } if (!serverOptionsBuilder.isLoopAvailable()) { serverOptionsBuilder.loopResources(HttpResources.get()); } this.options = serverOptionsBuilder.build(); this.server = new TcpBridgeServer(this.options); }
/** * Get a copy of the {@link HttpServerOptions} currently in effect. * * @return the http server options */ public final HttpServerOptions options() { return this.options.duplicate(); }
@Override public String toString() { return "HttpServer: " + options.asSimpleString(); }
@Override @SuppressWarnings("unchecked") public Mono extends NettyContext> newHandler(BiFunction super HttpServerRequest, ? super HttpServerResponse, ? extends Publisher Objects.requireNonNull(handler, "handler"); return server.newHandler((BiFunction }
/** * Define routes for the server through the provided {@link HttpServerRoutes} builder. * * @param routesBuilder provides a route builder to be mutated in order to define routes. * @return a new {@link Mono} starting the router on subscribe */ public Mono extends NettyContext> newRouter(Consumer super HttpServerRoutes> routesBuilder) { Objects.requireNonNull(routesBuilder, "routeBuilder"); HttpServerRoutes routes = HttpServerRoutes.newRoutes(); routesBuilder.accept(routes); return newHandler(routes); }
/** * Start an HttpServer with routes defined through the provided {@link HttpServerRoutes} * builder, in a blocking fashion, and wait for it to finish initializing. * The returned {@link BlockingNettyContext} class offers a simplified API around operating * the client/server in a blocking fashion, including to {@link BlockingNettyContext#shutdown() shut it down}. * * @param routesBuilder provides a route builder to be mutated in order to define routes. * @return a {@link BlockingNettyContext} */ public BlockingNettyContext startRouter(Consumer super HttpServerRoutes> routesBuilder) { Objects.requireNonNull(routesBuilder, "routeBuilder"); HttpServerRoutes routes = HttpServerRoutes.newRoutes(); routesBuilder.accept(routes); return start(routes); }
/** * Start an HttpServer with routes defined through the provided {@link HttpServerRoutes} * builder, in a fully blocking fashion, not only waiting for it to * initialize but also blocking during the full lifecycle of the server. * Since most servers will be long-lived, this is more adapted to running a server * out of a main method, only allowing shutdown of the servers through sigkill. * * Note that a {@link Runtime#addShutdownHook(Thread) JVM shutdown hook} is added * by this method in order to properly disconnect the client/server upon receiving * a sigkill signal. * * @param routesBuilder provides a route builder to be mutated in order to define routes. */ public void startRouterAndAwait(Consumer super HttpServerRoutes> routesBuilder) { startRouterAndAwait(routesBuilder, null); }
/** * Start an HttpServer with routes defined through the provided {@link HttpServerRoutes} * builder, in a fully blocking fashion, not only waiting for it to * initialize but also blocking during the full lifecycle of the server. * Since most servers will be long-lived, this is more adapted to running a server * out of a main method, only allowing shutdown of the servers through sigkill. * * Note that a {@link Runtime#addShutdownHook(Thread) JVM shutdown hook} is added * by this method in order to properly disconnect the client/server upon receiving * a sigkill signal. * * @param routesBuilder provides a route builder to be mutated in order to define routes. * @param onStart an optional callback to be invoked once the server has finished initializing. */ public void startRouterAndAwait(Consumer super HttpServerRoutes> routesBuilder, Consumer Objects.requireNonNull(routesBuilder, "routeBuilder"); HttpServerRoutes routes = HttpServerRoutes.newRoutes(); routesBuilder.accept(routes); startAndAwait(routes, onStart); }
static final LoggingHandler loggingHandler = new LoggingHandler(HttpServer.class);
static final boolean ACCESS_LOG_ENABLED = Boolean.parseBoolean(System.getProperty("reactor.netty.http.server.accessLogEnabled", "false"));
static BiPredicate HttpServerOptions options) {
int minResponseSize = options.minCompressionResponseSize();
if (minResponseSize < 0) { return null; }
if (minResponseSize == 0) { return options.compressionPredicate(); }
BiPredicate (req, res) -> { String length = res.responseHeaders() .get(HttpHeaderNames.CONTENT_LENGTH);
if (length == null) { return true; }
try { return Long.parseLong(length) >= minResponseSize; } catch (NumberFormatException nfe) { return true; } };
if (options.compressionPredicate() == null) { return lengthPredicate; }
return lengthPredicate.and(options.compressionPredicate()); }
final class TcpBridgeServer extends TcpServer implements BiConsumer
TcpBridgeServer(ServerOptions options) { super(options); }
@Override protected ContextHandler BiFunction super NettyInbound, ? super NettyOutbound, ? extends Publisher MonoSink
BiPredicate compressPredicate(options);
boolean alwaysCompress = compressPredicate == null && options.minCompressionResponseSize() == 0;
return ContextHandler.newServerContext(sink, options, loggingHandler, (ch, c, msg) -> {
HttpServerOperations ops = HttpServerOperations.bindHttp(ch, handler, c, compressPredicate, msg);
if (alwaysCompress) { ops.compression(true); }
return ops; }) .onPipeline(this) .autoCreateOperations(false); }
@Override public void accept(ChannelPipeline p, ContextHandler p.addLast(NettyPipeline.HttpCodec, new HttpServerCodec( options.httpCodecMaxInitialLineLength(), options.httpCodecMaxHeaderSize(), options.httpCodecMaxChunkSize(), options.httpCodecValidateHeaders(), options.httpCodecInitialBufferSize())); // TODO 这里增加了HttpAggregator 100000 100k Properties properties = System.getProperties(); Integer maxLength = Integer.valueOf(String.valueOf(properties.getOrDefault("max.httpcontent.length", "100000"))); p.addLast(NettyPipeline.HttpAggregator, new HttpObjectAggregator(maxLength));
if (ACCESS_LOG_ENABLED) { p.addLast(NettyPipeline.AccessLogHandler, new AccessLogHandler()); }
p.addLast(NettyPipeline.HttpServerHandler, new HttpServerHandler(c)); }
@Override protected LoggingHandler loggingHandler() { return loggingHandler; } }
public static final class Builder { private String bindAddress = null; private int port = 8080; private Supplier private Consumer super HttpServerOptions.Builder> options;
private Builder() { }
/** * The address to listen for (e.g. 0.0.0.0 or 127.0.0.1) * * @param bindAddress address to listen for (e.g. 0.0.0.0 or 127.0.0.1) * @return {@code this} */ public final Builder bindAddress(String bindAddress) { this.bindAddress = Objects.requireNonNull(bindAddress, "bindAddress"); return this; }
/** * The {@link InetSocketAddress} to listen on. * * @param listenAddress the listen address * @return {@code this} */ public final Builder listenAddress(InetSocketAddress listenAddress) { Objects.requireNonNull(listenAddress, "listenAddress"); this.listenAddress = () -> listenAddress; return this; }
/** * The port to listen to, or 0 to dynamically attribute one. * * @param port the port to listen to, or 0 to dynamically attribute one. * @return {@code this} */ public final Builder port(int port) { this.port = port; return this; }
/** * The options for the server, including bind address and port. * * @param options the options for the server, including bind address and port. * @return {@code this} */ public final Builder options(Consumer super HttpServerOptions.Builder> options) { this.options = Objects.requireNonNull(options, "options"); return this; }
public HttpServer build() { return new HttpServer(this); } } } |