跳至主要內容

SpringCloudGateway-1

wangdx大约 15 分钟

SpringCloudGateway 自动装配类

1、
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gateway-core</artifactId>
      <version>2.2.2.RELEASE</version>
      <scope>compile</scope>
    </dependency>

2、
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayHystrixCircuitBreakerAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayResilience4JCircuitBreakerAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration,\
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration,\
org.springframework.cloud.gateway.config.SimpleUrlHandlerMappingGlobalCorsAutoConfiguration,\
org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration

org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.cloud.gateway.config.GatewayEnvironmentPostProcessor

GatewayAutoConfiguration

1、
package org.springframework.cloud.gateway.config;
@Configuration(proxyBeanMethods = false)
// 如果要想让此配置类生效,应该在配置文件之中提供有“spring.cloud.gateway.enabled”配置项,若没有为true
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties // 注入一些相关的属性内容
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, // HTTP处理类
      WebFluxAutoConfiguration.class }) // 配置WebFlux处理类
// 处理完成之后还需要提供有相关的Bean对象
@AutoConfigureAfter({ GatewayLoadBalancerClientAutoConfiguration.class,
      GatewayClassPathWarningAutoConfiguration.class })
@ConditionalOnClass(DispatcherHandler.class) // 分发的处理
public class GatewayAutoConfiguration {
   @Bean
   public StringToZonedDateTimeConverter stringToZonedDateTimeConverter() {
      return new StringToZonedDateTimeConverter();// 字符串与日期时间转换处理
   }
   @Bean
   public RouteLocatorBuilder routeLocatorBuilder(// 路由定位器构建器
         ConfigurableApplicationContext context) {
      return new RouteLocatorBuilder(context);
   }
   @Bean
   @ConditionalOnMissingBean
   public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(
         GatewayProperties properties) { // 提供有相关的路由的配置属性
      return new PropertiesRouteDefinitionLocator(properties);
   }
// 面试题:SpringCloudGateway之中所有的路由信息保存在什么位置?
// 回答:所有的路由信息保存在网关的内存之中,这样可以快速读取配置;
   @Bean
   @ConditionalOnMissingBean(RouteDefinitionRepository.class)
   public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
      return new InMemoryRouteDefinitionRepository();// 在内存中保存路由的定义存储
   }
   @Bean
   @Primary
   public RouteDefinitionLocator routeDefinitionLocator(
         List<RouteDefinitionLocator> routeDefinitionLocators) {
      return new CompositeRouteDefinitionLocator(
            Flux.fromIterable(routeDefinitionLocators));
   }
   @Bean
   public ConfigurationService gatewayConfigurationService(BeanFactory beanFactory,
         @Qualifier("webFluxConversionService") ObjectProvider<ConversionService> conversionService,
         ObjectProvider<Validator> validator) {
      return new ConfigurationService(beanFactory, conversionService, validator);
   }
   @Bean
   public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
         List<GatewayFilterFactory> gatewayFilters,
         List<RoutePredicateFactory> predicates,
         RouteDefinitionLocator routeDefinitionLocator,
         ConfigurationService configurationService) { // 根据构造器创建路由定位器
      return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates,
            gatewayFilters, properties, configurationService);
   }
   @Bean
   @Primary
   @ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
   public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
      return new CachingRouteLocator(// 缓存路由定位器
            new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
   }
   @Bean
   public RouteRefreshListener routeRefreshListener(// 路由刷新监听
         ApplicationEventPublisher publisher) {
      return new RouteRefreshListener(publisher); // 允许动态修改路由配置
   }
   @Bean
   public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters) {
      return new FilteringWebHandler(globalFilters);
   }
   @Bean
   public GlobalCorsProperties globalCorsProperties() {
      return new GlobalCorsProperties();
   }
   @Bean
   public RoutePredicateHandlerMapping routePredicateHandlerMapping(
         FilteringWebHandler webHandler, RouteLocator routeLocator,
         GlobalCorsProperties globalCorsProperties, Environment environment) {
      return new RoutePredicateHandlerMapping(webHandler, routeLocator,
            globalCorsProperties, environment);
   }
   @Bean
   public GatewayProperties gatewayProperties() { // 网关配置的属性内容
      return new GatewayProperties();
   }
   @Bean
   public SecureHeadersProperties secureHeadersProperties() {
      return new SecureHeadersProperties();
   }
   @Bean
   @ConditionalOnProperty(name = "spring.cloud.gateway.forwarded.enabled",
         matchIfMissing = true)
   public ForwardedHeadersFilter forwardedHeadersFilter() {
      return new ForwardedHeadersFilter();// 过滤器
   }
   @Bean
   public RemoveHopByHopHeadersFilter removeHopByHopHeadersFilter() {
      return new RemoveHopByHopHeadersFilter();
   }
   @Bean
   @ConditionalOnProperty(name = "spring.cloud.gateway.x-forwarded.enabled",
         matchIfMissing = true)
   public XForwardedHeadersFilter xForwardedHeadersFilter() {
      return new XForwardedHeadersFilter();
   }
   @Bean
   public AdaptCachedBodyGlobalFilter adaptCachedBodyGlobalFilter() {
      return new AdaptCachedBodyGlobalFilter();
   }
   @Bean
   public RemoveCachedBodyFilter removeCachedBodyFilter() {
      return new RemoveCachedBodyFilter();
   }
   @Bean
   public RouteToRequestUrlFilter routeToRequestUrlFilter() {
      return new RouteToRequestUrlFilter();
   }
   @Bean
   public ForwardRoutingFilter forwardRoutingFilter(
         ObjectProvider<DispatcherHandler> dispatcherHandler) {
      return new ForwardRoutingFilter(dispatcherHandler);
   }
   @Bean
   public ForwardPathFilter forwardPathFilter() {
      return new ForwardPathFilter();
   }
   @Bean
   public WebSocketService webSocketService() {
      return new HandshakeWebSocketService();
   }
   @Bean
   public WebsocketRoutingFilter websocketRoutingFilter(WebSocketClient webSocketClient,
         WebSocketService webSocketService,
         ObjectProvider<List<HttpHeadersFilter>> headersFilters) {
      return new WebsocketRoutingFilter(webSocketClient, webSocketService,
            headersFilters);
   }
   @Bean
   public WeightCalculatorWebFilter weightCalculatorWebFilter(
         ConfigurationService configurationService,
         ObjectProvider<RouteLocator> routeLocator) {
      return new WeightCalculatorWebFilter(routeLocator, configurationService);
   }
   @Bean
   public AfterRoutePredicateFactory afterRoutePredicateFactory() {
      return new AfterRoutePredicateFactory();
   }
   @Bean
   public BeforeRoutePredicateFactory beforeRoutePredicateFactory() {
      return new BeforeRoutePredicateFactory();
   }
   @Bean
   public BetweenRoutePredicateFactory betweenRoutePredicateFactory() {
      return new BetweenRoutePredicateFactory();
   }
   @Bean
   public CookieRoutePredicateFactory cookieRoutePredicateFactory() {
      return new CookieRoutePredicateFactory();
   }
   @Bean
   public HeaderRoutePredicateFactory headerRoutePredicateFactory() {
      return new HeaderRoutePredicateFactory();
   }
   @Bean
   public HostRoutePredicateFactory hostRoutePredicateFactory() {
      return new HostRoutePredicateFactory();
   }
   @Bean
   public MethodRoutePredicateFactory methodRoutePredicateFactory() {
      return new MethodRoutePredicateFactory();
   }
   @Bean
   public PathRoutePredicateFactory pathRoutePredicateFactory() {
      return new PathRoutePredicateFactory();
   }
   @Bean
   public QueryRoutePredicateFactory queryRoutePredicateFactory() {
      return new QueryRoutePredicateFactory();
   }
   @Bean
   public ReadBodyPredicateFactory readBodyPredicateFactory() {
      return new ReadBodyPredicateFactory();
   }
   @Bean
   public RemoteAddrRoutePredicateFactory remoteAddrRoutePredicateFactory() {
      return new RemoteAddrRoutePredicateFactory();
   }
   @Bean
   @DependsOn("weightCalculatorWebFilter")
   public WeightRoutePredicateFactory weightRoutePredicateFactory() {
      return new WeightRoutePredicateFactory();
   }
   @Bean
   public CloudFoundryRouteServiceRoutePredicateFactory cloudFoundryRouteServiceRoutePredicateFactory() {
      return new CloudFoundryRouteServiceRoutePredicateFactory();
   }
   @Bean
   public AddRequestHeaderGatewayFilterFactory addRequestHeaderGatewayFilterFactory() {
      return new AddRequestHeaderGatewayFilterFactory();
   }
   @Bean
   public MapRequestHeaderGatewayFilterFactory mapRequestHeaderGatewayFilterFactory() {
      return new MapRequestHeaderGatewayFilterFactory();
   }
   @Bean
   public AddRequestParameterGatewayFilterFactory addRequestParameterGatewayFilterFactory() {
      return new AddRequestParameterGatewayFilterFactory();
   }
   @Bean
   public AddResponseHeaderGatewayFilterFactory addResponseHeaderGatewayFilterFactory() {
      return new AddResponseHeaderGatewayFilterFactory();
   }
   @Bean
   public ModifyRequestBodyGatewayFilterFactory modifyRequestBodyGatewayFilterFactory(
         ServerCodecConfigurer codecConfigurer) {
      return new ModifyRequestBodyGatewayFilterFactory(codecConfigurer.getReaders());
   }
   @Bean
   public DedupeResponseHeaderGatewayFilterFactory dedupeResponseHeaderGatewayFilterFactory() {
      return new DedupeResponseHeaderGatewayFilterFactory();
   }
   @Bean
   public ModifyResponseBodyGatewayFilterFactory modifyResponseBodyGatewayFilterFactory(
         ServerCodecConfigurer codecConfigurer, Set<MessageBodyDecoder> bodyDecoders,
         Set<MessageBodyEncoder> bodyEncoders) {
      return new ModifyResponseBodyGatewayFilterFactory(codecConfigurer.getReaders(),
            bodyDecoders, bodyEncoders);
   }
   @Bean
   public PrefixPathGatewayFilterFactory prefixPathGatewayFilterFactory() {
      return new PrefixPathGatewayFilterFactory();
   }
   @Bean
   public PreserveHostHeaderGatewayFilterFactory preserveHostHeaderGatewayFilterFactory() {
      return new PreserveHostHeaderGatewayFilterFactory();
   }
   @Bean
   public RedirectToGatewayFilterFactory redirectToGatewayFilterFactory() {
      return new RedirectToGatewayFilterFactory();
   }
   @Bean
   public RemoveRequestHeaderGatewayFilterFactory removeRequestHeaderGatewayFilterFactory() {
      return new RemoveRequestHeaderGatewayFilterFactory();
   }
   @Bean
   public RemoveRequestParameterGatewayFilterFactory removeRequestParameterGatewayFilterFactory() {
      return new RemoveRequestParameterGatewayFilterFactory();
   }
   @Bean
   public RemoveResponseHeaderGatewayFilterFactory removeResponseHeaderGatewayFilterFactory() {
      return new RemoveResponseHeaderGatewayFilterFactory();
   }

   @Bean(name = PrincipalNameKeyResolver.BEAN_NAME)
   @ConditionalOnBean(RateLimiter.class)
   @ConditionalOnMissingBean(KeyResolver.class)
   public PrincipalNameKeyResolver principalNameKeyResolver() {
      return new PrincipalNameKeyResolver();
   }
   @Bean
   @ConditionalOnBean({ RateLimiter.class, KeyResolver.class })
   public RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory(
         RateLimiter rateLimiter, KeyResolver resolver) {
      return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver);
   }
   @Bean
   public RewritePathGatewayFilterFactory rewritePathGatewayFilterFactory() {
      return new RewritePathGatewayFilterFactory();
   }
   @Bean
   public RetryGatewayFilterFactory retryGatewayFilterFactory() {
      return new RetryGatewayFilterFactory();
   }
   @Bean
   public SetPathGatewayFilterFactory setPathGatewayFilterFactory() {
      return new SetPathGatewayFilterFactory();
   }
   @Bean
   public SecureHeadersGatewayFilterFactory secureHeadersGatewayFilterFactory(
         SecureHeadersProperties properties) {
      return new SecureHeadersGatewayFilterFactory(properties);
   }
   @Bean
   public SetRequestHeaderGatewayFilterFactory setRequestHeaderGatewayFilterFactory() {
      return new SetRequestHeaderGatewayFilterFactory();
   }
   @Bean
   public SetResponseHeaderGatewayFilterFactory setResponseHeaderGatewayFilterFactory() {
      return new SetResponseHeaderGatewayFilterFactory();
   }
   @Bean
   public RewriteResponseHeaderGatewayFilterFactory rewriteResponseHeaderGatewayFilterFactory() {
      return new RewriteResponseHeaderGatewayFilterFactory();
   }
   @Bean
   public RewriteLocationResponseHeaderGatewayFilterFactory rewriteLocationResponseHeaderGatewayFilterFactory() {
      return new RewriteLocationResponseHeaderGatewayFilterFactory();
   }
   @Bean
   public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() {
      return new SetStatusGatewayFilterFactory();
   }
   @Bean
   public SaveSessionGatewayFilterFactory saveSessionGatewayFilterFactory() {
      return new SaveSessionGatewayFilterFactory();
   }
   @Bean
   public StripPrefixGatewayFilterFactory stripPrefixGatewayFilterFactory() {
      return new StripPrefixGatewayFilterFactory();
   }
   @Bean
   public RequestHeaderToRequestUriGatewayFilterFactory requestHeaderToRequestUriGatewayFilterFactory() {
      return new RequestHeaderToRequestUriGatewayFilterFactory();
   }
   @Bean
   public RequestSizeGatewayFilterFactory requestSizeGatewayFilterFactory() {
      return new RequestSizeGatewayFilterFactory();
   }
   @Bean
   public RequestHeaderSizeGatewayFilterFactory requestHeaderSizeGatewayFilterFactory() {
      return new RequestHeaderSizeGatewayFilterFactory();
   }
   @Bean
   public GzipMessageBodyResolver gzipMessageBodyResolver() {
      return new GzipMessageBodyResolver();
   }
   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass(HttpClient.class)
   protected static class NettyConfiguration { // 进行Netty相关配置
      protected final Log logger = LogFactory.getLog(getClass());
      @Bean
      @ConditionalOnProperty(name = "spring.cloud.gateway.httpserver.wiretap")
      public NettyWebServerFactoryCustomizer nettyServerWiretapCustomizer(
            Environment environment, ServerProperties serverProperties) {
         return new NettyWebServerFactoryCustomizer(environment, serverProperties) {
            @Override
            public void customize(NettyReactiveWebServerFactory factory) {
               factory.addServerCustomizers(httpServer -> httpServer.wiretap(true));
               super.customize(factory);
            }
         };
      }
      @Bean
      @ConditionalOnMissingBean
      public HttpClient gatewayHttpClient(HttpClientProperties properties,
            List<HttpClientCustomizer> customizers) {
         HttpClientProperties.Pool pool = properties.getPool();
         ConnectionProvider connectionProvider;
         if (pool.getType() == DISABLED) {
            connectionProvider = ConnectionProvider.newConnection();
         }
         else if (pool.getType() == FIXED) {
            connectionProvider = ConnectionProvider.fixed(pool.getName(),
                  pool.getMaxConnections(), pool.getAcquireTimeout(),
                  pool.getMaxIdleTime(), pool.getMaxLifeTime());
         }
         else {
            connectionProvider = ConnectionProvider.elastic(pool.getName(),
                  pool.getMaxIdleTime(), pool.getMaxLifeTime());
         }
         HttpClient httpClient = HttpClient.create(connectionProvider)
               // TODO: move customizations to HttpClientCustomizers
               .httpResponseDecoder(spec -> {
                  if (properties.getMaxHeaderSize() != null) {
                     // cast to int is ok, since @Max is Integer.MAX_VALUE
                     spec.maxHeaderSize(
                           (int) properties.getMaxHeaderSize().toBytes());
                  }
                  if (properties.getMaxInitialLineLength() != null) {
                     // cast to int is ok, since @Max is Integer.MAX_VALUE
                     spec.maxInitialLineLength(
                           (int) properties.getMaxInitialLineLength().toBytes());
                  }
                  return spec;
               }).tcpConfiguration(tcpClient -> {

                  if (properties.getConnectTimeout() != null) {
                     tcpClient = tcpClient.option(
                           ChannelOption.CONNECT_TIMEOUT_MILLIS,
                           properties.getConnectTimeout());
                  }

                  // configure proxy if proxy host is set.
                  HttpClientProperties.Proxy proxy = properties.getProxy();

                  if (StringUtils.hasText(proxy.getHost())) {

                     tcpClient = tcpClient.proxy(proxySpec -> {
                        ProxyProvider.Builder builder = proxySpec
                              .type(ProxyProvider.Proxy.HTTP)
                              .host(proxy.getHost());

                        PropertyMapper map = PropertyMapper.get();

                        map.from(proxy::getPort).whenNonNull().to(builder::port);
                        map.from(proxy::getUsername).whenHasText()
                              .to(builder::username);
                        map.from(proxy::getPassword).whenHasText()
                              .to(password -> builder.password(s -> password));
                        map.from(proxy::getNonProxyHostsPattern).whenHasText()
                              .to(builder::nonProxyHosts);
                     });
                  }
                  return tcpClient;
               });

         HttpClientProperties.Ssl ssl = properties.getSsl();
         if ((ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0)
               || ssl.getTrustedX509CertificatesForTrustManager().length > 0
               || ssl.isUseInsecureTrustManager()) {
            httpClient = httpClient.secure(sslContextSpec -> {
               // configure ssl
               SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();

               X509Certificate[] trustedX509Certificates = ssl
                     .getTrustedX509CertificatesForTrustManager();
               if (trustedX509Certificates.length > 0) {
                  sslContextBuilder = sslContextBuilder
                        .trustManager(trustedX509Certificates);
               }
               else if (ssl.isUseInsecureTrustManager()) {
                  sslContextBuilder = sslContextBuilder
                        .trustManager(InsecureTrustManagerFactory.INSTANCE);
               }

               try {
                  sslContextBuilder = sslContextBuilder
                        .keyManager(ssl.getKeyManagerFactory());
               }
               catch (Exception e) {
                  logger.error(e);
               }

               sslContextSpec.sslContext(sslContextBuilder)
                     .defaultConfiguration(ssl.getDefaultConfigurationType())
                     .handshakeTimeout(ssl.getHandshakeTimeout())
                     .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout())
                     .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout());
            });
         }

         if (properties.isWiretap()) {
            httpClient = httpClient.wiretap(true);
         }

         if (!CollectionUtils.isEmpty(customizers)) {
            customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
            for (HttpClientCustomizer customizer : customizers) {
               httpClient = customizer.customize(httpClient);
            }
         }

         return httpClient;
      }

      @Bean
      public HttpClientProperties httpClientProperties() {
         return new HttpClientProperties();
      }

      @Bean
      public NettyRoutingFilter routingFilter(HttpClient httpClient,
            ObjectProvider<List<HttpHeadersFilter>> headersFilters,
            HttpClientProperties properties) {
         return new NettyRoutingFilter(httpClient, headersFilters, properties);
      }

      @Bean
      public NettyWriteResponseFilter nettyWriteResponseFilter(
            GatewayProperties properties) {
         return new NettyWriteResponseFilter(properties.getStreamingMediaTypes());
      }

      @Bean
      public ReactorNettyWebSocketClient reactorNettyWebSocketClient(
            HttpClientProperties properties, HttpClient httpClient) {
         ReactorNettyWebSocketClient webSocketClient = new ReactorNettyWebSocketClient(
               httpClient);
         if (properties.getWebsocket().getMaxFramePayloadLength() != null) {
            webSocketClient.setMaxFramePayloadLength(
                  properties.getWebsocket().getMaxFramePayloadLength());
         }
         webSocketClient.setHandlePing(properties.getWebsocket().isProxyPing());
         return webSocketClient;
      }
   }
   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass({ HystrixObservableCommand.class, RxReactiveStreams.class })
   protected static class HystrixConfiguration { // Hystrix相关配置
      @Bean
      public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(
            ObjectProvider<DispatcherHandler> dispatcherHandler) {
         return new HystrixGatewayFilterFactory(dispatcherHandler);
      }
      @Bean
      @ConditionalOnMissingBean(FallbackHeadersGatewayFilterFactory.class)
      public FallbackHeadersGatewayFilterFactory fallbackHeadersGatewayFilterFactory() {
         return new FallbackHeadersGatewayFilterFactory();
      }
   }
   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass(Health.class)
   protected static class GatewayActuatorConfiguration { // Actuator监控服务配置
      @Bean
      @ConditionalOnProperty(name = "spring.cloud.gateway.actuator.verbose.enabled",
            matchIfMissing = true)
      @ConditionalOnAvailableEndpoint
      public GatewayControllerEndpoint gatewayControllerEndpoint(
            List<GlobalFilter> globalFilters,
            List<GatewayFilterFactory> gatewayFilters,
            List<RoutePredicateFactory> routePredicates,
            RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
         return new GatewayControllerEndpoint(globalFilters, gatewayFilters,
               routePredicates, routeDefinitionWriter, routeLocator);
      }
      @Bean
      @Conditional(OnVerboseDisabledCondition.class)
      @ConditionalOnAvailableEndpoint
      public GatewayLegacyControllerEndpoint gatewayLegacyControllerEndpoint(
            RouteDefinitionLocator routeDefinitionLocator,
            List<GlobalFilter> globalFilters,
            List<GatewayFilterFactory> gatewayFilters,
            List<RoutePredicateFactory> routePredicates,
            RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) {
         return new GatewayLegacyControllerEndpoint(routeDefinitionLocator,
               globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter,
               routeLocator);
      }
   }
   private static class OnVerboseDisabledCondition extends NoneNestedConditions {
      OnVerboseDisabledCondition() {
         super(ConfigurationPhase.REGISTER_BEAN);
      }
      @ConditionalOnProperty(name = "spring.cloud.gateway.actuator.verbose.enabled",
            matchIfMissing = true)
      static class VerboseDisabled {
      }
   }
}

RouteLocator

1、
@Bean
public RouteLocator routeDefinitionRouteLocator(GatewayProperties properties,
      List<GatewayFilterFactory> gatewayFilters,
      List<RoutePredicateFactory> predicates,
      RouteDefinitionLocator routeDefinitionLocator,
      ConfigurationService configurationService) {
   return new RouteDefinitionRouteLocator(routeDefinitionLocator, predicates,
         gatewayFilters, properties, configurationService);
}
@Bean
@Primary
@ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
   return new CachingRouteLocator(
         new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}


2、
package org.springframework.cloud.gateway.route;
import reactor.core.publisher.Flux;
public interface RouteLocator {
   Flux<Route> getRoutes();// 此时数据将采用异步返回的形式处理,同时可以获取多个路由信息
}


3、
package org.springframework.cloud.gateway.route;
public class Route implements Ordered {
   private final String id; // 路由的标记
   private final URI uri; // 路由的访问地址
   private final int order; // 顺序,与Ordered有关
   private final AsyncPredicate<ServerWebExchange> predicate;  // 断言
   private final List<GatewayFilter> gatewayFilters; // 网关过滤
   private final Map<String, Object> metadata; // 元数据信息
}


4、
package org.springframework.cloud.gateway.route;
@Validated // 【JSR303验证标准】该配置Bean必须验证(后面会有动态路由配置)
public class RouteDefinition {
   private String id;
   @NotEmpty // 非空验证
   @Valid
   private List<PredicateDefinition> predicates = new ArrayList<>();
   @Valid // 验证
   private List<FilterDefinition> filters = new ArrayList<>();
   @NotNull
   private URI uri;
   private Map<String, Object> metadata = new HashMap<>();
   private int order = 0;
}

FilteringWebHandler

1、
@Bean
public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters) {
   return new FilteringWebHandler(globalFilters); // 需要保存全部的全局过滤器
}


2、
package org.springframework.cloud.gateway.handler;
public class FilteringWebHandler implements WebHandler {
   protected static final Log logger = LogFactory.getLog(FilteringWebHandler.class);
   private final List<GatewayFilter> globalFilters; // 网关过滤器
   public FilteringWebHandler(List<GlobalFilter> globalFilters) { // 全局过滤器
      this.globalFilters = loadFilters(globalFilters); // 加载全局过滤器
   }
   private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
// 将GlobalFilter接口的全部子类实例,转为了GatewayFilter实例
      return filters.stream().map(filter -> { // Stream处理,此时的filter为GlobalFilter实例
// 创建GatewayFilter的实现子类对象实例(适配器的处理类)
         GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);
         if (filter instanceof Ordered) { // 是否是Ordered接口子类
            int order = ((Ordered) filter).getOrder();// 获取执行顺序,数字越小越先执行
            return new OrderedGatewayFilter(gatewayFilter, order); // GatewayFilter子类
         }
         return gatewayFilter;
      }).collect(Collectors.toList());// 将最终的处理结果转为List集合返回
   }
   @Override // ServerWebExchange可以获取到Request/Response对象实例
   public Mono<Void> handle(ServerWebExchange exchange) { // WebHandler接口的方法
      Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR); // 获取路由
      List<GatewayFilter> gatewayFilters = route.getFilters();// 获取网关过滤
      List<GatewayFilter> combined = new ArrayList<>(this.globalFilters); // 组合过滤器
      combined.addAll(gatewayFilters); // 添加网关过滤
      AnnotationAwareOrderComparator.sort(combined); // 进行过滤排序
      if (logger.isDebugEnabled()) {
         logger.debug("Sorted gatewayFilterFactories: " + combined);
      }
      return new DefaultGatewayFilterChain(combined).filter(exchange); // 过滤链
   }
   private static class DefaultGatewayFilterChain implements GatewayFilterChain {
      private final int index;
      private final List<GatewayFilter> filters;
      DefaultGatewayFilterChain(List<GatewayFilter> filters) {
         this.filters = filters;
         this.index = 0;
      }
      private DefaultGatewayFilterChain(DefaultGatewayFilterChain parent, int index) {
         this.filters = parent.getFilters();
         this.index = index;
      }
      public List<GatewayFilter> getFilters() {
         return filters;
      }
      @Override
      public Mono<Void> filter(ServerWebExchange exchange) {
         return Mono.defer(() -> {
            if (this.index < filters.size()) {
               GatewayFilter filter = filters.get(this.index);
               DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this,
                     this.index + 1);
               return filter.filter(exchange, chain);
            }
            else {
               return Mono.empty(); // complete
            }
         });
      }
   }
   private static class GatewayFilterAdapter implements GatewayFilter {
      private final GlobalFilter delegate;
      GatewayFilterAdapter(GlobalFilter delegate) {
         this.delegate = delegate;
      }
      @Override
      public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
         return this.delegate.filter(exchange, chain);
      }
      @Override
      public String toString() {
         final StringBuilder sb = new StringBuilder("GatewayFilterAdapter{");
         sb.append("delegate=").append(delegate);
         sb.append('}');
         return sb.toString();
      }
   }
}

RoutePredicateHandlerMapping

1、
@Bean
public RoutePredicateHandlerMapping routePredicateHandlerMapping(
      FilteringWebHandler webHandler, RouteLocator routeLocator,
      GlobalCorsProperties globalCorsProperties, Environment environment) {
   return new RoutePredicateHandlerMapping(webHandler, routeLocator,
         globalCorsProperties, environment);
}


2、
package org.springframework.cloud.gateway.handler;
public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
   private final FilteringWebHandler webHandler;  // 网关过滤器
   private final RouteLocator routeLocator; // 网关路由定位
   private final Integer managementPort; // 管理端口
   private final ManagementPortType managementPortType; // 端口类型
   public RoutePredicateHandlerMapping(FilteringWebHandler webHandler,
         RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties,
         Environment environment) {
      this.webHandler = webHandler;
      this.routeLocator = routeLocator;
      this.managementPort = getPortProperty(environment, "management.server.");
      this.managementPortType = getManagementPortType(environment);
      setOrder(1); // 设置执行顺序
      setCorsConfigurations(globalCorsProperties.getCorsConfigurations());
   }
   private ManagementPortType getManagementPortType(Environment environment) {
      Integer serverPort = getPortProperty(environment, "server."); // 获取端口内容
      if (this.managementPort != null && this.managementPort < 0) {
         return DISABLED; // 端口类型的处理
      }
      return ((this.managementPort == null
            || (serverPort == null && this.managementPort.equals(8080))
            || (this.managementPort != 0 && this.managementPort.equals(serverPort)))
                  ? SAME : DIFFERENT);
   }
   private static Integer getPortProperty(Environment environment, String prefix) {
      return environment.getProperty(prefix + "port", Integer.class); // 处理属性
   }
   @Override
   protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
      // don't handle requests on management port if set and different than server port
      if (this.managementPortType == DIFFERENT && this.managementPort != null
            && exchange.getRequest().getURI().getPort() == this.managementPort) {
         return Mono.empty();// 端口处理类型是否符合预期,直接返回空
      }
      exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());
      return lookupRoute(exchange) // 查找路由
            .flatMap((Function<Route, Mono<?>>) r -> {
               exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
               if (logger.isDebugEnabled()) {
                  logger.debug(
                        "Mapping [" + getExchangeDesc(exchange) + "] to " + r);
               }
               exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
               return Mono.just(webHandler); // 进行WEB处理
            }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
               exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
               if (logger.isTraceEnabled()) {
                  logger.trace("No RouteDefinition found for ["
                        + getExchangeDesc(exchange) + "]");
               }
            })));
   }
   @Override
   protected CorsConfiguration getCorsConfiguration(Object handler,
         ServerWebExchange exchange) {
      return super.getCorsConfiguration(handler, exchange);
   }
   private String getExchangeDesc(ServerWebExchange exchange) {
      StringBuilder out = new StringBuilder();
      out.append("Exchange: ");
      out.append(exchange.getRequest().getMethod());
      out.append(" ");
      out.append(exchange.getRequest().getURI());
      return out.toString();
   }
   protected Mono<Route> lookupRoute(ServerWebExchange exchange) { // 路由查找
      return this.routeLocator.getRoutes() // 通过路由定位器获取全部的路由
            .concatMap(route -> Mono.just(route).filterWhen(r -> {
               // 对当前的路由进行一些测试处理
               exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
               return r.getPredicate().apply(exchange); // 路由的处理
            })
                  .doOnError(e -> logger.error(// 路由错误
                        "Error applying predicate for route: " + route.getId(),
                        e))
                  .onErrorResume(e -> Mono.empty()))
            .next()
            .map(route -> { // 路由处理
               if (logger.isDebugEnabled()) {
                  logger.debug("Route matched: " + route.getId());
               }
               validateRoute(route, exchange); // 路由校验
               return route; // 返回路由对象
            });
   }
   @SuppressWarnings("UnusedParameters")
   protected void validateRoute(Route route, ServerWebExchange exchange) {
   }
   protected String getSimpleName() {
      return "RoutePredicateHandlerMapping";
   }
   public enum ManagementPortType { // 端口的类型定义
      DISABLED, // 服务端口关闭
      SAME, // 与应用的端口相同
      DIFFERENT; // 与应用的端口不同
   }
}

动态路由简介

微服务核心架构

  • 微服务架构中引入了网关技术之后,就可以得到如图的完整核心架构,在整个的架构之中网关将成为某一个资源能否被正常调用的关键核心。

动态网关技术

  • 在传统的网关开发过程中,所有需要网关代理的资源都必须通过 application.yml 文件进行定义,而这些配置全部都是静态方式实现的,即:当有某一个新的微服务资源上线否则将时,除了要考虑到新资源的服务部署之外,还需要进行微服务网关的重新启动,无法加载到新的资源,而网关的重启过程中势必将影响其他微服务的正常运行而要想解决此类问题就必须引入动态网关技术进行资源管理

动态路由配置实现结构

  • 在进行路由设置时需要明确的传递路由的 ID、Predicate、Filter 等配置项,所以一般可以通过一个 JSON 的结构来进行此配置项的定义,而在 SpringCloudGateway 中路由的信息是由 RouteDefinition 类对象定义的,这样就可以直接将 JSON 数据转为 RouteDefinition 实例,随后利用 RouteDefinitionWriter 接口进行配置写入,而要想让动态路由生效,则还必须通过 Spring 事件处理机制发送一“RefreshRoutesEvent”事件,这样才可以将写入的路由信息进行保存。

动态路由模型

动态路由操作

  • 动态路由的操作需要通过 RouteDefinitionWriter 接口完成,而该接口主要是通过路由 ID 以及 RouteDefinition 对象实例实现操作。所以为了便于操作的统一性,最佳的做法是创建一个专属的动态路由服务类,并在该类中提供路由数据的增加、修改与删除操作同时为了便于网关数据的管理需要,可以通过 REST 进行操作接口发布这样只需要传,下面通过具体的步骤进行实现。入正确的数据即可进行网关维护,
1、
spring:
  application:
    name: microcloud.gateway # 网关名称
  cloud: # Cloud配置
    loadbalancer:
      ribbon:
        enabled: false # 关闭默认配置
    nacos: # Nacos注册中心配置
      discovery: # 发现服务
        server-addr: nacos-server:8848 # Nacos服务地址
        namespace: 96c23d77-8d08-4648-b750-1217845607ee # 命名空间ID
        group: MICROCLOUD_GROUP # 一般建议大写
        cluster-name: MuyanGateway # 配置集群名称
        username: muyan # 用户名
        password: yootk # 密码
    gateway: # 网关配置
      metrics:
        enabled: true # 启用服务监控
      discovery: # 服务发现
        locator: # 资源定位
          enabled: false # 取消默认路由配置,默认值就是false
      routes: # 定义静态路由
        - id: yootk_example # 配置路由ID
          uri: https://www.yootk.com/resources # 设置访问路径的匹配
          predicates:
            - Path=/muyan-yootk # 配置访问路径
        - id: forward_example # 配置路由ID
          uri: forward:///globalforward # 配置本地转发
          predicates:
            - Path=/globalforward # 配置访问路径
          filters:
            - PrefixPath=/gateway/action # 路径前缀
        - id: dept # 路由标记
          uri: lb://dept.provider # 负载均衡调用
          predicates: # 路由谓词工厂
            - Path=/** # 匹配全部的路径
          filters: # 配置过滤器
            - RemoveRequestHeader=Request-Token-Muyan # 删除指定的头信息
            - Log=muyan, yootk # 过滤器=NameValueConfig(name属性, value属性)

2、
spring:
  application:
    name: microcloud.gateway # 网关名称
  cloud: # Cloud配置
    loadbalancer:
      ribbon:
        enabled: false # 关闭默认配置
    nacos: # Nacos注册中心配置
      discovery: # 发现服务
        server-addr: nacos-server:8848 # Nacos服务地址
        namespace: 96c23d77-8d08-4648-b750-1217845607ee # 命名空间ID
        group: MICROCLOUD_GROUP # 一般建议大写
        cluster-name: MuyanGateway # 配置集群名称
        username: muyan # 用户名
        password: yootk # 密码
    gateway: # 网关配置
      metrics:
        enabled: true # 启用服务监控
      discovery: # 服务发现
        locator: # 资源定位
          enabled: false # 取消默认路由配置,默认值就是false

3、
package com.yootk.gateway.service;

import javassist.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
@Slf4j
public class DynamicRouteService implements ApplicationEventPublisherAware {
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter; // 路由数据的写入
    private ApplicationEventPublisher publisher; // 事件发布器

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher; // 保存事件发布器
    }

    public boolean add(RouteDefinition definition) {    // 追加新的路由配置
        log.info("增加路由配置项,新的路由ID为:{}", definition.getId()); // 日志输出
        try {
            this.routeDefinitionWriter.save(Mono.just(definition)).subscribe(); // 配置写入
            this.publisher.publishEvent(new RefreshRoutesEvent(this)); // 发布路由事件
        } catch (Exception e) {
            e.printStackTrace();
            log.error("路由增加失败,增加的路由ID为:{}", definition.getId());
            return false;
        }
        return true;
    }

    public Mono<ResponseEntity<Object>> delete(String id) { // 根据id删除数据
        log.info("删除路由配置项,删除的路由ID为:{}", id); // 日志输出
        return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> {
            return Mono.just(ResponseEntity.ok().build());
        })).onErrorResume((t) -> {
            return t instanceof NotFoundException;
        }, (r) -> {
            return Mono.just(ResponseEntity.notFound().build());
        });
    }

    public boolean update(RouteDefinition definition) {    // 修改已有的路由配置
        log.info("更新路由配置项,新的路由ID为:{}", definition.getId()); // 日志输出
        try {
            this.delete(definition.getId()); // 根据ID删除已有路由
            this.routeDefinitionWriter.save(Mono.just(definition)).subscribe(); // 配置写入
            this.publisher.publishEvent(new RefreshRoutesEvent(this)); // 发布路由事件
        } catch (Exception e) {
            log.error("路由更新失败,增加的路由ID为:{}", definition.getId());
            return false;
        }
        return true;
    }
}



4、
package com.yootk.gateway.action;

import com.yootk.gateway.service.DynamicRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/routes/*") // 访问父路径
public class DynamicRouteAction { // 动态路由
    @Autowired
    private DynamicRouteService dynamicRouteService; // 路由业务对象
    @PostMapping("add")
    public Boolean add(@RequestBody RouteDefinition definition) {
        return this.dynamicRouteService.add(definition);
    }
    @DeleteMapping("delete/{id}")
    public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
        return this.dynamicRouteService.delete(id);
    }
    @PostMapping("update")
    public Boolean update(@RequestBody RouteDefinition definition) {
        return this.dynamicRouteService.update(definition);
    }
}


5、
gateway-9501:9501/routes/add

6、
{
  "id": "dept",
  "uri": "lb://dept.provider",
  "order": 1,
  "predicates": [
    {
      "name": "Path",
      "args": {
        "pattern": "/**"
      }
    }
  ],
  "filters": [
    {
      "name": "AddRequestHeader",
      "args": {
        "_genkey_0": "Request-Token-Muyan",
        "_genkey_1": "www.yootk.com"
      }
    }
  ]
}

7、
gateway-9501:9501/routes/update


{
  "id": "dept",
  "uri": "lb://dept.provider",
  "order": 1,
  "predicates": [
    {
      "name": "Path",
      "args": {
        "pattern": "/provider/dept/list"
      }
    }
  ],
  "filters": [
    {
      "name": "AddRequestHeader",
      "args": {
        "_genkey_0": "Request-Token-Muyan",
        "_genkey_1": "www.yootk.com"
      }
    }
  ]
}

8、
gateway-9501:9501/routes/delete/dept

9、
gateway-9501:9090/actuator/gateway/routes

动态路由配置持久化

Nacos 网关数据持久化

  • 网关数据除了可以动态配置之外,还需要进行有效的持久化管理,这样才可以保证在每次服务启动之后网关配置数据不会丢失,同时在进行数据持久化配置时,还需要充分的考虑持久化配置数据发生改变时,网关也要及时进行更新处理,所以最佳的做法就是通过 Nacos 进行数据存储
1、
[
  {
    "id": "dept",
    "uri": "lb://dept.provider",
    "order": 1,
    "predicates": [
      {
        "name": "Path",
        "args": {
          "pattern": "/**"
        }
      }
    ],
    "filters": [
      {
        "name": "AddRequestHeader",
        "args": {
          "_genkey_0": "Request-Token-Muyan",
          "_genkey_1": "www.yootk.com"
        }
      }
    ]
  }
]

2、
project(":gateway-9501") {  // 网关模块
    dependencies {
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation(libraries.'nacos-client') // 引入与当前的Nacos匹配的依赖库
        implementation('org.springframework.cloud:spring-cloud-starter-gateway') // 网关依赖
        implementation('org.springframework.boot:spring-boot-starter-actuator') // Actuator依赖库
        implementation('org.springframework.cloud:spring-cloud-starter-loadbalancer')
        implementation(libraries.'caffeine')
        implementation(libraries.'micrometer-registry-prometheus')
        implementation(libraries.'micrometer-core')
    }
}

3、
package com.yootk.gateway.config;

import com.alibaba.nacos.api.PropertyKeyConst;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Properties;

@Component
@Data
@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery")
public class GatewayNacosConfig { // 自定义配置存储类
    private String serverAddr;
    private String namespace;
    private String group;
    private String username;
    private String password;
    private String dataId = "gateway.config";
    private long timeout = 2000;
    public Properties getNacosProperties() {
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, this.serverAddr);
        properties.put(PropertyKeyConst.NAMESPACE, this.namespace);
        properties.put(PropertyKeyConst.USERNAME, this.username);
        properties.put(PropertyKeyConst.PASSWORD, this.password);
        return properties;
    }
}


4、
package com.yootk.gateway.listener;

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yootk.gateway.config.GatewayNacosConfig;
import com.yootk.gateway.service.DynamicRouteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;

import java.util.concurrent.Executor;

@Component
@Slf4j
public class GatewayNacosRouteListener implements CommandLineRunner {
    @Autowired
    private DynamicRouteService dynamicRouteService; // 设置业务层处理
    @Autowired
    private GatewayNacosConfig nacosConfig; // Nacos服务配置
    // 因为Nacos里面保存的数据类型是JSON数据,所以需要对JSON进行解析,直接使用Jackson组件了
    @Autowired
    private ObjectMapper mapper; // 获取Jackson组件

    @Override
    public void run(String... args) throws Exception {
        this.nacosDynmaicRouteListener();// 启动时加载配置
    }

    public void nacosDynmaicRouteListener() {   // 动态路由监听
        try {
            ConfigService configService = NacosFactory.createConfigService(this.nacosConfig.getNacosProperties());
            String content = configService.getConfig(this.nacosConfig.getDataId(), this.nacosConfig.getGroup(), this.nacosConfig.getTimeout()); // 获取指定的配置项
            log.info("【网关启动】读取Nacos网关配置项:{}", content); // 日志输出
            GatewayNacosRouteListener.this.setRoute(content); // 路由配置
            configService.addListener(this.nacosConfig.getDataId(), this.nacosConfig.getGroup(), new Listener() {
                @Override
                public Executor getExecutor() {
                    return null;
                }

                @Override
                public void receiveConfigInfo(String configInfo) {
                    log.info("【网关更新】读取Nacos网关配置项:{}", configInfo); // 日志输出
                    GatewayNacosRouteListener.this.setRoute(configInfo);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void setRoute(String configInfo) { // 定义路由处理
        try {   // 将读取到的数据内容转为路由的配置定义,本操作是由Jackson组件完成的
            RouteDefinition[] routes = this.mapper.readValue(configInfo, RouteDefinition[].class);
            for (RouteDefinition route : routes) {
                this.dynamicRouteService.update(route); // 业务更新
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


5、
[
  {
    "id": "dept",
    "uri": "lb://dept.provider",
    "order": 1,
    "predicates": [
      {
        "name": "Path",
        "args": {
          "pattern": "/provider/dept/list"
        }
      }
    ],
    "filters": [
      {
        "name": "AddRequestHeader",
        "args": {
          "_genkey_0": "Request-Token-Muyan",
          "_genkey_1": "www.yootk.com"
        }
      }
    ]
  }
]

demo


上次编辑于: