spring cloud gateway 二次开发

 

最近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.classRibbonAutoConfiguration.classDispatcherHandler.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 的启动环境 WebApplicationTypeREACTIVE才会进行加载

而这个类型怎么确定呢?
当我们依赖 spring-boot-starter-web 的时候,WebApplicationTypeSERVLET
依赖 spring-boot-starter-webflux 并且 没有依赖 spring-boot-starter-web 的时候,WebApplicationTypeREACTIVE
如果没有依赖,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/**

ModifyRequestBodyMyLog 是过滤器的名字,他们都继承了AbstractGatewayFilterFactory,这样gateway在启动的时候,就会把配置的名字加上GatewayFilterFactory的后缀作为bean的名字进行加载,在这个例子中就是 ModifyRequestBodyGatewayFilterFactoryMyLogGatewayFilterFactory 这个两个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
 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值