前言 网上关于zuul
聚合各个微服务的swagger
已经有很多文章了, 但是没有一个完整的关于spring cloud gateway
的聚合教程. 研究了一天, 写了个demo
, 然后写篇文章记录一下.
Swagger引入 Springfox
现在已经更新到3.0.0
了, 并且也支持了starter
方式. 直接引入 , 不需要加任何注解.
1 2 3 4 5 <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-boot-starter</artifactId > <version > 3.0.0</version > </dependency >
聚合Swagger 和zuul
的套路一样, 我们要实现SwaggerResourcesProvider
接口, 然后根据各个服务的服务名拼接uri
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Primary @Configuration(proxyBeanMethods = false) public class SwaggerConfig implements SwaggerResourcesProvider { @Autowired private RouteDefinitionLocator routeDefinitionLocator; @Override public List<SwaggerResource> get () { List<SwaggerResource> swaggerResourceList = new ArrayList <>(); routeDefinitionLocator.getRouteDefinitions() .map(this ::swaggerResource) .subscribe(swaggerResourceList::add); return swaggerResourceList; } private SwaggerResource swaggerResource (RouteDefinition definition) { String location = definition.getPredicates().stream() .filter(predicate -> "Path" .equalsIgnoreCase(predicate.getName())) .findFirst() .map(PredicateDefinition::getArgs) .map(map -> map.getOrDefault("pattern" , map.get(NameUtils.GENERATED_NAME_PREFIX + "0" ))) .map(pattern -> StringUtils.substringBefore(pattern, "*" )) .map(pattern -> pattern + "v2/api-docs" ) .orElse(null ); SwaggerResource swaggerResource = new SwaggerResource (); swaggerResource.setName(definition.getId()); swaggerResource.setLocation(location); return swaggerResource; } }
整个执行流程很简单, 就是从路由定义 里面获取路由规则 , 拼接成swagger
的地址, 丢到SwaggerResource
里面. 但是中间有一些比较坑的地方.
Flux 转化为 List 对象 不懂Mono
和Flux
的去搜索, 自己实现一个webflux
的hello world
程序.Mono
和Flux
都是支持泛型的类. 可以简单地认为, Mono
是包装了一个对象的对象, Flux
是包装了一堆对象的集合. 要进行转换有两种方法
1 2 3 4 5 6 7 8 9 List<SwaggerResource> swaggerResourceList1 = new ArrayList <>(); routeDefinitionLocator.getRouteDefinitions() .map(this ::swaggerResource) .subscribe(swaggerResourceList1::add); List<SwaggerResource> swaggerResourceList2 = routeDefinitionLocator.getRouteDefinitions() .map(this ::swaggerResource) .collectList() .block(Duration.ofSeconds(10 ));
但是我们只能用第一种, 第二种方法会抛出IllegalStateException
异常.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public abstract class Mono <T> implements CorePublisher <T> { public T block (Duration timeout) { BlockingMonoSubscriber<T> subscriber = new BlockingMonoSubscriber <>(); subscribe((Subscriber<T>) subscriber); return subscriber.blockingGet(timeout.toMillis(), TimeUnit.MILLISECONDS); } } abstract class BlockingSingleSubscriber <T> extends CountDownLatch implements InnerConsumer <T>, Disposable { final T blockingGet (long timeout, TimeUnit unit) { if (Schedulers.isInNonBlockingThread()) { throw new IllegalStateException ("block()/blockFirst()/blockLast() are blocking, which is not supported in thread " + Thread.currentThread().getName()); } } } public abstract class Schedulers { public static boolean isInNonBlockingThread () { return Thread.currentThread() instanceof NonBlocking; } }
获取路由规则 gateway
会从注册中心获取服务的相关信息, 初始化路由规则.GatewayDiscoveryClientAutoConfiguration
会为DiscoveryLocatorProperties
属性生成一个初始化的PredicateDefinition
集合, 有点像原型模式.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 @Configuration(proxyBeanMethods = false) public class GatewayDiscoveryClientAutoConfiguration { public static List<PredicateDefinition> initPredicates () { ArrayList<PredicateDefinition> definitions = new ArrayList <>(); PredicateDefinition predicate = new PredicateDefinition (); predicate.setName("Path" ); predicate.addArg("pattern" , "'/'+serviceId+'/**'" ); definitions.add(predicate); return definitions; } @Bean public DiscoveryLocatorProperties discoveryLocatorProperties () { DiscoveryLocatorProperties properties = new DiscoveryLocatorProperties (); properties.setPredicates(initPredicates()); properties.setFilters(initFilters()); return properties; } } public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLocator { @Override public Flux<RouteDefinition> getRouteDefinitions () { return serviceInstances.filter(instances -> !instances.isEmpty()) .map(instances -> instances.get(0 )).filter(includePredicate) .map(instance -> { String serviceId = instance.getServiceId(); RouteDefinition routeDefinition = new RouteDefinition (); routeDefinition.setId(this .routeIdPrefix + serviceId); String uri = urlExpr.getValue(evalCtxt, instance, String.class); routeDefinition.setUri(URI.create(uri)); final ServiceInstance instanceForEval = new DelegatingServiceInstance (instance, properties); for (PredicateDefinition original : this .properties.getPredicates()) { PredicateDefinition predicate = new PredicateDefinition (); predicate.setName(original.getName()); for (Map.Entry<String, String> entry : original.getArgs().entrySet()) { String value = parser.parseExpression(entry.getValue()).getValue(evalCtxt, instanceForEval, String.class); predicate.addArg(entry.getKey(), value); } routeDefinition.getPredicates().add(predicate); } return routeDefinition; }); } }
_genkey_0是从哪来的? 在上面的源码分析里已经看到了pattern
这个key
是从哪里生成的了. 但是为什么还有一个_genkey_0
呢?
因为如果是手动在配置文件里面配置服务的话, gateway
会自动生成key
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public final class NameUtils { public static final String GENERATED_NAME_PREFIX = "_genkey_" ; public static String generateName (int i) { return GENERATED_NAME_PREFIX + i; } } public class PredicateDefinition { public PredicateDefinition (String text) { int eqIdx = text.indexOf('=' ); if (eqIdx <= 0 ) { throw new ValidationException ("Unable to parse PredicateDefinition text '" + text + "'" + ", must be of the form name=value" ); } setName(text.substring(0 , eqIdx)); String[] args = tokenizeToStringArray(text.substring(eqIdx + 1 ), "," ); for (int i = 0 ; i < args.length; i++) { this .args.put(NameUtils.generateName(i), args[i]); } } }
看到这, 你就会发现, 我写的代码其实有一个问题, 如果配置文件里有多个predicate
. 那么一定要保证Path
是第0
个, 否则就获取不到路由规则. 目前也没想到什么好办法, 就这样将就用吧.
服务id特别长怎么办 有的同学会发现, 注册中心生成的服务id
特别长, 不美观.我只想安安静静做一个美男子 我只想要显示服务名, 可以把前缀都去掉吗?
答案是不行. 在配置文件中加入配置项spring.cloud.gateway.discovery.locator.route-id-prefix: "你想要的前缀"
即可. 但是不能填空字符串.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLocator { private DiscoveryClientRouteDefinitionLocator (String discoveryClientName, DiscoveryLocatorProperties properties) { this .properties = properties; if (StringUtils.hasText(properties.getRouteIdPrefix())) { routeIdPrefix = properties.getRouteIdPrefix(); } else { routeIdPrefix = discoveryClientName + "_" ; } evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build(); } }
总结 实际的例子可以看我在github
上的项目ahao-spring-cloud-gateway