最近spring cloud gateway 二次开发时看到文章,里面处理获取返回体内容有用到,转载过来一起学习
1、ServerListUpdater 服务列表更新
gateway使用ribbon作为服务调用的负载均衡中间件,根据配置的 IRule 对拉取到的服务列表进行负载
而这些真正提供服务的实例是有动态上下线的情况存在的,为了保证轮询到的服务实例能正常访问,ribbon中有一个接口
ServerListUpdater 会定期对服务列表进行更新
在使用 Eureka 作为注册中心的时候,ServerListUpdater有两个实现类:
-
PollingServerListUpdater :定时从注册中心拉取服务列表,如果没有配置,默认为30秒
-
EurekaNotificationServerListUpdater :注册中心中的服务有变动时,通知客户端,EurekaNotificationServerListUpdater是通过添加了一个监听器,当收到注册中心的通知后,做出相应的动作
PollingServerListUpdater 也是 默认的 ServerListUpdater 配置
分别看一下它们的代码实现
PollingServerListUpdater
// 从注册中心拉取服务列表的间隔
private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs;
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
// 这里封装了一个进行服务拉取的任务
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
//这里是进行服务列表更新的动作
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
// 使用 scheduled 进行定时拉取
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,//这个就是刚才配置的拉取间隔
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
EurekaNotificationServerListUpdater
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
this.updateListener = new EurekaEventListener() {
// 监听 Eureka发布的事件,然后拉取最新的列表
@Override
public void onEvent(EurekaEvent event) {
if (event instanceof CacheRefreshedEvent) {
if (!updateQueued.compareAndSet(false, true)) { // if an update is already queued
logger.info("an update action is already queued, returning as no-op");
return;
}
if (!refreshExecutor.isShutdown()) {
try {
refreshExecutor.submit(new Runnable() {
@Override
public void run() {
try {
//这里是进行服务列表更新的动作
updateAction.doUpdate();
lastUpdated.set(System.currentTimeMillis());
} catch (Exception e) {
logger.warn("Failed to update serverList", e);
} finally {
updateQueued.set(false);
}
}
}); // fire and forget
} catch (Exception e) {
logger.warn("Error submitting update task to executor, skipping one round of updates", e);
updateQueued.set(false); // if submit fails, need to reset updateQueued to false
}
}
else {
logger.debug("stopping EurekaNotificationServerListUpdater, as refreshExecutor has been shut down");
stop();
}
}
}
};
...以下代码省略
} else {
logger.info("Update listener already registered, no-op");
}
}
可以看到,这两个 ServerListUpdater 实现类在更新服务列表的时候,都做了同一个动作updateAction.doUpdate()
进入这个方法,发现它是一个定义在ServerListUpdater 中的一个接口
public interface ServerListUpdater {
/**
* an interface for the updateAction that actually executes a server list update
*/
public interface UpdateAction {
void doUpdate();
}
...以下代码省略
}
而它的方法实现是在 负载均衡器 DynamicServerListLoadBalancer 中定义的
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicServerListLoadBalancer.class);
......代码省略......
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
// UpdateAction的方法实现,调用了另一个方法
updateListOfServers();
}
};
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
//真正进行列表更新的地方
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
......代码省略......
}
可以看到,这个 serverListImpl 就是在负载均衡器的ServerList属性,使用这个接口的getUpdatedListOfServers方法进行列表更新,因为我的项目里使用的是自己的注册中心,没有用Eureka,所以也写了一个实现类,参照了使用Eureka的情况下的默认类 DiscoveryEnabledNIWSServerList,让我们看看这个类的代码
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
//其实在更新的时候,调用的也是从注册中心拉取列表的方法
return obtainServersViaDiscovery();
}
//从Eureka拉取服务
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
......代码省略......
return serverList;
}
由此我们知道,无论配置哪个 ServerListUpdater,在更新服务列表的时候,都是调用ServerList接口进行一次服务拉取,然后更新本地的列表,只是触发的时机不同:
- PollingServerListUpdater 30秒拉取一次(时间可以修改)
- EurekaNotificationServerListUpdater 当服务更新时,通知客户端
那么这两种方法分别有什么弊端呢?
先说说PollingServerListUpdater,如果在拉取的间隔中,有服务下线了,极端情况下,原来所有的实例都不可用,换成了新的一批实例,要等到下一次拉取的时间点才会更新,这样会造成最久30秒的时间服务不可用。 比如:原来5个实例 A、B、C、D、E变成了 F、G、H、I、J,但是由于并没有到更新的时间点,ribbon保存的还是老的服务实例,而这时它们都已下线,无法提供服务。
那如果我们换成,一旦服务变更(这里是上下线都会通知)就通知客户端的 EurekaNotificationServerListUpdater 会怎么样呢,这样好像可以实时的替换成最新的可用实例,保证服务不会打到失效的实例上。可是这会有另一个问题,也是在极端情况下,如果这次通知由于网络问题,没有通知到客户端,那么这次变动过后,如果一直没有服务变更,客户端就再也不会进行服务的拉取,这个时候造成不可用的时间就难以预估了。比如:在9:00的时候,A、B、C、D、E服务全部下线,F、G、H、I、J上线,Eureka通知客户端,但网络抖动,客户端没有收到,或者客户端收到了,拉取时候失败,并没有更新本地的列表,这样只有等到下次收到通知时才会去拉取,假设接下来服务很稳定,12:00的时候才有一次更新,这样就有3个小时的服务不可用。
那么有没有什么办法既可以拥有2个实现类的优势,又摒弃它们的弊端呢?有的!
小孩子才做选择,我们大人是全都要!那就是自己写一个ServerListUpdater的实现类,然后:
- 1 启动一个定时任务,定时从注册中心拉取,而这个拉取间隔也可以根据自己的预估进行修改。
- 2 注册一个监听器,可以收到注册中心的服务变更通知
- 3 在配置中,把默认的 ServerListUpdater改成自己写的 ServerListUpdater,这样ribbonConfig类在看到有ServerListUpdater的实现类的情况下,就不会加载 Eureka的 ServerListUpdater了。
实现类的代码太长,就不贴了,逻辑很清晰,可以参考PollingServerListUpdater 和 EurekaNotificationServerListUpdater,把他们的代码照着写就行
接下来就是配置,在配置类中添加上自己的实现类
/**
* 服务更新通知机制
* @param notificationService
* @param clientConfig
* @return
*/
@Bean
public ServerListUpdater ribbonServerListUpdater(NotificationServiceImpl notificationService, IClientConfig clientConfig) {
return new PollingNotificationServerListUpdater(notificationService, clientConfig);
}
2、 Ribbon重试配置
gateway使用ribbon作为服务调用的负载均衡中间件,最终使用的都是ribbon的几个组件实现:
- ServerList:拉取服务列表接口
- ServerListFilter:对ServerList服务器列表进行过滤
- ServerListUpdater: 更新服务列表的接口
- IPing: 健康检查
- IRule :负载均衡规则,如轮询、随机等
- ILoadBalancer:负载均衡器,组合以上组件最终实现负载
对于以上接口的实现先暂时不写,这次写的是gateway使用以上组件进行负载均衡的2种实现方式:
一 使用gateway自己的过滤器 LoadBalancerClientFilter 组合以上接口 实现
二 通过对 RestTemplate 增加 LoadBalancerInterceptor拦截器实现
那么项目在启动的时候是如何在这两种方式当中选择的呢?
我们先看第一种方式,LoadBalancerClientFilter的加载条件
@Configuration
@ConditionalOnClass({ LoadBalancerClient.class, RibbonAutoConfiguration.class,
DispatcherHandler.class })
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
public class GatewayLoadBalancerClientAutoConfiguration {
// GlobalFilter beans
@Bean
@ConditionalOnBean(LoadBalancerClient.class)
@ConditionalOnMissingBean(LoadBalancerClientFilter.class)
public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client,
LoadBalancerProperties properties) {
return new LoadBalancerClientFilter(client, properties);
}
}
是通过 GatewayLoadBalancerClientAutoConfiguration 配置类来加载的,而GatewayLoadBalancerClientAutoConfiguration的加载条件是,当存在
LoadBalancerClient.class ,RibbonAutoConfiguration.class , DispatcherHandler.class的时候会加载,
1 LoadBalancerClient.class 只有一个默认实现 RibbonLoadBalancerClient,是肯定会加载的
我们看剩下2个类
2 RibbonAutoConfiguration 配置类 是当我们在项目中引入 spring-cloud-starter-netflix-ribbon 依赖的时候进行加载
3 剩下最后一个DispatcherHandler 这个是关键,我在排查问题的时候,一直忽略这个类,看这名字感觉一定会加载,没想到这个才是重点所在
DispatcherHandler 是在 WebFluxConfigurationSupport 中定义的
public class WebFluxConfigurationSupport implements ApplicationContextAware {
...中间代码省略
@Bean
public DispatcherHandler webHandler() {
return new DispatcherHandler();
}
...以下代码省略
}
WebFluxConfigurationSupport 被 DelegatingWebFluxConfiguration 继承
@Configuration
public class DelegatingWebFluxConfiguration extends WebFluxConfigurationSupport {
可以看到,DelegatingWebFluxConfiguration 又被 EnableWebFluxConfiguration 继承
而 EnableWebFluxConfiguration 被定义在 WebFluxAutoConfiguration 这个配置类中
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@ConditionalOnMissingBean({ WebFluxConfigurationSupport.class })
@AutoConfigureAfter({ ReactiveWebServerFactoryAutoConfiguration.class, CodecsAutoConfiguration.class,
ValidationAutoConfiguration.class })
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebFluxAutoConfiguration {
...中间代码省略
/**
* Configuration equivalent to {@code @EnableWebFlux}.
*/
@Configuration
public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration {
...中间代码省略
}
...代码省略
}
终于到了关键的地方了
WebFluxAutoConfiguration 的第二行
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
表明了它的加载条件,只有当 springboot 的启动环境 WebApplicationType 为 REACTIVE才会进行加载
而这个类型怎么确定呢?
当我们依赖 spring-boot-starter-web 的时候,WebApplicationType 为 SERVLET
依赖 spring-boot-starter-webflux 并且 没有依赖 spring-boot-starter-web 的时候,WebApplicationType 为 REACTIVE
如果没有依赖,springboot2.0以上默认为 REACTIVE
既然这两种方式都能实现负载均衡,那有什么区别的,关键在于想要进行错误重试的时候
如果使用 LoadBalancerClientFilter方式实现负载均衡,重试的时候要在配置文件中这么配置
spring:
cloud:
gateway:
default-filters:
- name: Retry
args:
retries: 3
这样是对所有路由生效,或者指定单个路由
spring:
gateway:
routes:
- id: test
uri: lb://test
predicates:
- Path=/test/*
filters:
- name: Retry
args:
retries: 3
而使用 LoadBalancerInterceptor拦截器方式实现负载均衡,配置文件中这么配置
ribbon:
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 2
ReadTimeout: 2000
ConnectTimeout: 1500
OkToRetryOnAllOperations: true
3、动态路由注意事项
网关开发的过程中,因为有对某些服务进行动态的上下线的需求,所以进行了动态路由的开发,网上也有例子,实现方式就不赘述了,但这里有2个注意事项。
这两个错误很严重,一旦出现了之后,和路由有关的功能都无法再使用,需要重启项目
一 路由信息里的断言器信息不能为空
如果在添加路由信息的时候没有传断言器,比如这样
{
"id":"test",
"predicates":[],
"filters":[],
"uri":"lb://test",
"order":0
}
会抛出异常
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
Caused by: java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.get(ArrayList.java:429)
at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.combinePredicates(RouteDefinitionRouteLocator.java:221)
at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:143)
我们看路由信息类的时候,可以发现断言器是一个List,RouteDefinitionRouteLocator下边有这个方法,会先取List中第一个断言器,然后与之后的断言器做组合
private AsyncPredicate<ServerWebExchange> combinePredicates(
RouteDefinition routeDefinition) {
List<PredicateDefinition> predicates = routeDefinition.getPredicates();
//取出第一个断言器
AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition,
predicates.get(0));
//取出后续断言器
for (PredicateDefinition andPredicate : predicates.subList(1,
predicates.size())) {
AsyncPredicate<ServerWebExchange> found = lookup(routeDefinition,
andPredicate);
//第一个断言器和后续断言器做 and 操作
predicate = predicate.and(found);
}
return predicate;
}
如果没有填断言器信息,就会报数组越界异常
二 断言器的名字不能随便取
比如这里我取了一个 zuibuxing 的断言器名字
{
"id":"test",
"predicates":[
{
"name":"zuibuxing",
"args":{
"_genkey_0":"/test/**"
}
}
],
"filters":[
],
"uri":"lb://test",
"order":0
}
会报这个错误
reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: Unable to find RoutePredicateFactory with name zuibuxing
Caused by: java.lang.IllegalArgumentException: Unable to find RoutePredicateFactory with name zuibuxing
at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.lookup(RouteDefinitionRouteLocator.java:240)
at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.combinePredicates(RouteDefinitionRouteLocator.java:220)
at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:143)
说找不到名字为 zuibuxing 的断言器,这是因为保存自定义路由的时候,RouteDefinitionRouteLocato 会按照 name 去断言器Map 里去寻找
private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();
... 中间代码省略 ....
//按照名字寻找断言器
private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route,
PredicateDefinition predicate) {
RoutePredicateFactory<Object> factory = this.predicates.get(predicate.getName());
if (factory == null) {
throw new IllegalArgumentException(
"Unable to find RoutePredicateFactory with name "
+ predicate.getName());
}
...以下代码省略
}
RouteDefinitionRouteLocator 在这个Map中保存所有实现了 RoutePredicateFactory 的类,name是类名去掉RoutePredicateFactory,比如 AfterRoutePredicateFactory 断言器的名字是 After,这里有两个特殊的断言器 CloudFoundryRouteService,ReadBodyPredicateFactory,因为它们的类名没有以 RoutePredicateFactory 结尾,所以就用本身类名作为 断言器name
如果是随便写的,会导致找不到断言器报错,所以我们只能填gateway自己的断言器,或者自己实现了 RoutePredicateFactory 的断言器
4、配置默认filter
在开发网关的时候,我们可以为我们的所有路由器定义同样的filter,这样就不用为每个route单独进行配置,有几种实现方式:
一:通过配置继承了AbstractGatewayFilterFactory的bean来进行
在配置文件中增加default-filters
spring:
application:
name: gateway
profiles:
active: prod
cloud:
gateway:
default-filters:
- name: ModifyRequestBody
- name: MyLog
routes:
- id: route1
uri: lb://route1
predicates:
- Path=/route1/**
- id: route2
uri: lb://route2
predicates:
- Path=/route2/**
ModifyRequestBody 和 MyLog 是过滤器的名字,他们都继承了AbstractGatewayFilterFactory,这样gateway在启动的时候,就会把配置的名字加上GatewayFilterFactory的后缀作为bean的名字进行加载,在这个例子中就是 ModifyRequestBodyGatewayFilterFactory 和 MyLogGatewayFilterFactory 这个两个bean,其中ModifyRequestBodyGatewayFilterFactory是gateway自带的类,MyLogGatewayFilterFactory是我自己实现的类,大家可以通过这样的方式自定义自己的过滤器。
但这种方式有一个问题,要注意一下,众所周知,路由器除了配置文件,还可以通过java 代码的方式进行配置,如下:
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/test/**")
.uri("lb://test")
.order(0)
.id("test")
)
.build();
}
或者我们也可以自己进行动态路由的配置,网上有很多例子,我就不贴出来了
而上边那种通过yml或properties文件配置默认过滤器的方式,对 java代码配置的路由器不生效
只对配置文件和动态配置的路由生效(这个坑我进行了一上午的尝试对比才发现)
因为通过使用代码定义路由使用的是 RouteLocator 这个类
而通过配置文件或者外部存储器配置路由使用的是 RouteDefinitionLocator 的实现类进行的,有 4个实现类:
- PropertiesRouteDefinitionLocator ,从配置文件( 例如,YML / Properties 等 ) 读取
- RouteDefinitionRepository ,从存储器( 例如,内存 / Redis / MySQL 等 )读取(默认使用的是从内存)
- DiscoveryClientRouteDefinitionLocator ,从注册中心( 例如,Eureka / Zookeeper 等 )读取
- CompositeRouteDefinitionLocator ,组合多种 RouteDefinitionLocator 的实现
这里应该是,配置gateway自带的过滤器还是生效的,但是自己继承了AbstractGatewayFilterFactory的类就不生效,因为之前的项目配置过自带的Hystrix过滤器生效了
如果想让自己的过滤器对所有的路由生效,就用到这第二种方式
5、处理 requestBody
这次是需要在请求的入口和出口分别打印报文信息。处理GET请求的时候还好,POST请求有时候参数放在requestBody中,而且2.X的版本之后,spring cloud使用 spring5 webflux方式编程,在filter中处理过一次的requestBody,下游订阅者无法接收,网上找了很多都是对某个具体路由在编码中进行配置,如下
.route("rewrite_request_upper", r -> r.host("*.rewriterequestupper.org")
.filters(f -> f.prefixPath("/httpbin")
.addResponseHeader("X-TestHeader", "rewrite_request_upper")
.modifyRequestBody(String.class, String.class,
(exchange, s) -> {
return Mono.just(s.toUpperCase()+s.toUpperCase());
})
).uri(uri)
)
而我的项目中使用了动态路由,未来路由的增减是很常见的事,这种方式没法对所有的路由进行请求拦截,很不灵活。
最后参看源码,发现了一个ModifyRequestBodyGatewayFilterFactory类,里边有对requestBody的处理逻辑,然后把源码照搬了过来,由于我只需要打印日志,稍微改一下就行,别的代码就保留了。
/**
* 参照 ModifyRequestBodyGatewayFilterFactory 写的一个处理 requestBody的filter
*
* @author huangting
*/
@Component
public class RequestHandlerFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(RequestHandlerFilter.class);
private static final String METHOD_POST = "POST";
private static final String METHOD_GET = "GET";
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//POST和get的处理逻辑不一样
if (METHOD_POST.equals(exchange.getRequest().getMethodValue())) {
ServerRequest serverRequest = new DefaultServerRequest(exchange);
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(requestBody -> {
//打印请求报文
logRequestLog(request, requestBody);
return Mono.just(requestBody);
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
// .log("modify_request", Level.INFO)
.then(Mono.defer(() -> {
ServerHttpRequest decorator = decorate(exchange, headers, outputMessage);
return chain.filter(exchange.mutate().request(decorator).build());
}));
} else {
//打印请求报文
logRequestLog(request, null);
chain.filter(exchange);
}
return chain.filter(exchange);
}
ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
// httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
/**
* 打印请求日志
*
* @param request
*/
public void logRequestLog(ServerHttpRequest request, String requestBody) {
String requestParam = request.getQueryParams().toString();
logger.info("请求报文 URL:{},Method:{},headers:{},param:{},requestbody:{}", request.getURI().getPath(), request.getMethod(), request.getHeaders(), requestParam, requestBody);
}
@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -3;
}
}
需要注意的是 ,我的spring cloud 是 2.1.3,Greenwich.SR3的版本,CachedBodyOutputMessage 和 DefaultServerRequest这里两个类的权限变成了spring私有的了,需要把他们copy出来作为自己项目中的类,以上的代码有用到
6、处理 reponse报文(解决截断及乱码问题)
因为网关是请求的出入口,防止各调用方及服务方相互之间扯皮,响应报文也需要打出来,而这里有一个问题,如果响应过大的话,Flux会进行截断,这样有2个问题,一个是每次处理都会打一次部分报文,不过这个可以通过doOnComplete()来解决,第二就是达到一定长度出现乱码,后来查看api,有一个合并的方法,问题解决,代码如下
/**
* 处理响应的 的filter
* @author huangting
*/
@Component
public class ResponseHandlerFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(ResponseHandlerFilter.class);
private static final String START_TIME = "startTime";
@Autowired
private MetricService metricService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String ip = IpUtil.getRemoteHost(request);
//执行完成后 进行调用耗时埋点
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
//原始响应类
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
//重新包装的响应类
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
//如果响应过大,会进行截断,出现乱码,然后看api DefaultDataBufferFactory有个join方法可以合并所有的流,乱码的问题解决
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
//打印响应日志
logResponse(exchange, new String(content, StandardCharsets.UTF_8));
return bufferFactory.wrap(content);
}));
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(START_TIME);
if (startTime != null) {
Long executeTime = (System.currentTimeMillis() - startTime);
//influxDB埋点
metricService.pointRequestLatency(ip, request.getURI().getPath(), executeTime);
}
}));
}
/**
* 打印响应报文
*
* @param exchange
*/
public void logResponse(ServerWebExchange exchange, String response) {
ServerHttpRequest request = exchange.getRequest();
logger.info("响应报文 URL:{},Method:{},headers:{},response:{}", request.getURI().getPath(), request.getMethod(), exchange.getResponse().getHeaders(), response);
}
@Override
public int getOrder() {
// -1 is response write filter, must be called before that
return -3;
}
}
最近发现一个问题,当接口的调用返回值为空的时候,并不会进入 writeWith 里边的map方法,所以当Flux 进行订阅时,map里的 logResponse 没有执行,所以先定义一个默认的 AtomicReference<String> responseBody 变量,当返回值不为空时,对它进行更新;然后把 logResponse 方法下移,放到 return语句中,这样就能保证打印方法总会被执行,修改后的 filter 方法代码如下
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String ip = IpUtil.getRemoteHost(request);
//执行完成后 进行调用耗时埋点
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
//原始响应类
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
//初始化一个 默认的 responseBody
AtomicReference<String> responseBody= new AtomicReference<>("no-responseBody");
//重新包装的响应类
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffer -> {
//如果响应过大,会进行截断,出现乱码,然后看api DefaultDataBufferFactory有个join方法可以合并所有的流,乱码的问题解决
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
//释放掉内存
DataBufferUtils.release(join);
//如果有返回值,将 responseBody 覆盖
responseBody.set(new String(content, StandardCharsets.UTF_8));
return bufferFactory.wrap(content);
}));
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> {
//打印响应日志
logResponse(exchange, responseBody.get());
Long startTime = exchange.getAttribute(START_TIME);
if (startTime != null) {
Long executeTime = (System.currentTimeMillis() - startTime);
//influxDB埋点
metricService.pointRequestLatency(ip, request.getURI().getPath(), executeTime);
}
}));
}
文章转载 : 醉不醒 的spring cloud gateway 二次开发
链接:https://www.jianshu.com/p/a5fc8039d236