From 08b3d1a47417b0e51f8f655f2e569f864921b0dd Mon Sep 17 00:00:00 2001 From: lepdou Date: Mon, 4 Jul 2022 19:08:44 +0800 Subject: [PATCH] support spring cloud gateway routers (#388) --- CHANGELOG.md | 1 + .../pom.xml | 14 + .../router/config/FeignAutoConfiguration.java | 44 +++ .../config/RouterAutoConfiguration.java | 21 +- ...isLoadBalancerClientBeanPostProcessor.java | 68 +++++ ...larisReactiveLoadBalancerClientFilter.java | 264 ++++++++++++++++++ .../router/spi/RouterLabelResolver.java | 10 + .../main/resources/META-INF/spring.factories | 3 +- ...adBalancerClientBeanPostProcessorTest.java | 100 +++++++ ...sReactiveLoadBalancerClientFilterTest.java | 145 ++++++++++ .../gateway-callee-service2/pom.xml | 28 ++ .../callee/GatewayCalleeApplication.java | 35 +++ .../callee/GatewayCalleeController.java | 71 +++++ .../src/main/resources/bootstrap.yml | 10 + .../gateway-scg-service/pom.xml | 4 +- .../src/main/resources/bootstrap.yml | 4 - .../polaris-gateway-example/pom.xml | 3 +- .../example/CustomRouterLabelResolver.java | 1 - .../pom.xml | 20 +- .../loadbalancer/LoadBalancerUtils.java | 38 ++- .../loadbalancer/LoadBalancerUtilsTest.java | 106 +++++++ 21 files changed, 953 insertions(+), 37 deletions(-) create mode 100644 spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/FeignAutoConfiguration.java create mode 100644 spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilter.java create mode 100644 spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessorTest.java create mode 100644 spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilterTest.java create mode 100644 spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/pom.xml create mode 100644 spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeApplication.java create mode 100644 spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeController.java create mode 100644 spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/resources/bootstrap.yml create mode 100644 spring-cloud-tencent-polaris-loadbalancer/src/test/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtilsTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ceccf469..772273f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,3 +29,4 @@ - [docs: Fix javadoc
error](https://github.com/Tencent/spring-cloud-tencent/pull/375) - [docs: Update Readme.md](https://github.com/Tencent/spring-cloud-tencent/pull/381) - [docs:optimize example](https://github.com/Tencent/spring-cloud-tencent/pull/386) +- [Feature: support spring cloud gateway routers](https://github.com/Tencent/spring-cloud-tencent/pull/388) diff --git a/spring-cloud-starter-tencent-polaris-router/pom.xml b/spring-cloud-starter-tencent-polaris-router/pom.xml index 147451b5f..e21ca43f8 100644 --- a/spring-cloud-starter-tencent-polaris-router/pom.xml +++ b/spring-cloud-starter-tencent-polaris-router/pom.xml @@ -52,6 +52,20 @@ true + + + org.springframework.cloud + spring-cloud-gateway-server + true + + + + org.springframework.boot + spring-boot-starter-webflux + true + + + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/FeignAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/FeignAutoConfiguration.java new file mode 100644 index 000000000..89cea90d5 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/FeignAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.polaris.router.config; + +import java.util.List; + +import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.feign.RouterLabelFeignInterceptor; +import com.tencent.cloud.polaris.router.spi.RouterLabelResolver; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +@Configuration +@ConditionalOnClass(name = {"feign.RequestInterceptor"}) +public class FeignAutoConfiguration { + + @Bean + public RouterLabelFeignInterceptor routerLabelInterceptor(@Nullable List routerLabelResolvers, + MetadataLocalProperties metadataLocalProperties, + RouterRuleLabelResolver routerRuleLabelResolver) { + return new RouterLabelFeignInterceptor(routerLabelResolvers, metadataLocalProperties, routerRuleLabelResolver); + } + +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java index a9e1a54df..ef765fbdd 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/config/RouterAutoConfiguration.java @@ -18,21 +18,17 @@ package com.tencent.cloud.polaris.router.config; -import java.util.List; - -import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; import com.tencent.cloud.polaris.context.ServiceRuleManager; import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; -import com.tencent.cloud.polaris.router.feign.RouterLabelFeignInterceptor; import com.tencent.cloud.polaris.router.resttemplate.PolarisLoadBalancerBeanPostProcessor; -import com.tencent.cloud.polaris.router.spi.RouterLabelResolver; +import com.tencent.cloud.polaris.router.scg.PolarisLoadBalancerClientBeanPostProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.Order; -import org.springframework.lang.Nullable; import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE; @@ -47,16 +43,17 @@ import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE; public class RouterAutoConfiguration { @Bean - public RouterLabelFeignInterceptor routerLabelInterceptor(@Nullable List routerLabelResolvers, - MetadataLocalProperties metadataLocalProperties, - RouterRuleLabelResolver routerRuleLabelResolver) { - return new RouterLabelFeignInterceptor(routerLabelResolvers, metadataLocalProperties, routerRuleLabelResolver); + @Order(HIGHEST_PRECEDENCE) + @ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor") + public PolarisLoadBalancerBeanPostProcessor polarisLoadBalancerBeanPostProcessor() { + return new PolarisLoadBalancerBeanPostProcessor(); } @Bean @Order(HIGHEST_PRECEDENCE) - public PolarisLoadBalancerBeanPostProcessor polarisLoadBalancerBeanPostProcessor() { - return new PolarisLoadBalancerBeanPostProcessor(); + @ConditionalOnClass(name = "org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter") + public PolarisLoadBalancerClientBeanPostProcessor polarisLoadBalancerClientBeanPostProcessor() { + return new PolarisLoadBalancerClientBeanPostProcessor(); } @Bean diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessor.java new file mode 100644 index 000000000..ca45878da --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessor.java @@ -0,0 +1,68 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.polaris.router.scg; + +import java.util.List; + +import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; +import com.tencent.cloud.common.util.BeanFactoryUtils; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.RouterLabelResolver; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties; +import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; + +/** + * Replaced ReactiveLoadBalancerClientFilter with PolarisReactiveLoadBalancerClientFilter during creating bean phase. + *@author lepdou 2022-06-20 + */ +public class PolarisLoadBalancerClientBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { + + private BeanFactory factory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.factory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + // Support spring cloud gateway router. + // Replaces the default LoadBalancerClientFilter implementation and returns a custom PolarisLoadBalancerClientFilter + if (bean instanceof ReactiveLoadBalancerClientFilter) { + LoadBalancerClientFactory loadBalancerClientFactory = this.factory.getBean(LoadBalancerClientFactory.class); + GatewayLoadBalancerProperties gatewayLoadBalancerProperties = this.factory.getBean(GatewayLoadBalancerProperties.class); + LoadBalancerProperties loadBalancerProperties = this.factory.getBean(LoadBalancerProperties.class); + List routerLabelResolvers = BeanFactoryUtils.getBeans(factory, RouterLabelResolver.class); + MetadataLocalProperties metadataLocalProperties = this.factory.getBean(MetadataLocalProperties.class); + RouterRuleLabelResolver routerRuleLabelResolver = this.factory.getBean(RouterRuleLabelResolver.class); + + return new PolarisReactiveLoadBalancerClientFilter( + loadBalancerClientFactory, gatewayLoadBalancerProperties, loadBalancerProperties, + metadataLocalProperties, routerRuleLabelResolver, routerLabelResolvers); + } + return bean; + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilter.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilter.java new file mode 100644 index 000000000..b7cb97807 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilter.java @@ -0,0 +1,264 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.polaris.router.scg; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; +import com.tencent.cloud.common.util.ExpressionLabelUtils; +import com.tencent.cloud.common.util.JacksonUtils; +import com.tencent.cloud.polaris.router.RouterConstants; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.RouterLabelResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.CompletionContext; +import org.springframework.cloud.client.loadbalancer.DefaultRequest; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle; +import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator; +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools; +import org.springframework.cloud.client.loadbalancer.Request; +import org.springframework.cloud.client.loadbalancer.RequestData; +import org.springframework.cloud.client.loadbalancer.RequestDataContext; +import org.springframework.cloud.client.loadbalancer.Response; +import org.springframework.cloud.client.loadbalancer.ResponseData; +import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter; +import org.springframework.cloud.gateway.support.DelegatingServiceInstance; +import org.springframework.cloud.gateway.support.NotFoundException; +import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; +import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; + +/** + * ReactiveLoadBalancerClientFilter does not have the ability to pass route labels, so it is replaced with PolarisReactiveLoadBalancerClientFilter. + * The passed route labels are used in {@link com.tencent.cloud.polaris.router.PolarisRouterServiceInstanceListSupplier}. + *@author lepdou 2022-06-20 + */ +public class PolarisReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter { + private static final Logger log = LoggerFactory.getLogger(PolarisReactiveLoadBalancerClientFilter.class); + + private final LoadBalancerClientFactory clientFactory; + private final GatewayLoadBalancerProperties gatewayLoadBalancerProperties; + private final LoadBalancerProperties loadBalancerProperties; + private final MetadataLocalProperties metadataLocalProperties; + private final RouterRuleLabelResolver routerRuleLabelResolver; + private final List routerLabelResolvers; + + public PolarisReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, + GatewayLoadBalancerProperties gatewayLoadBalancerProperties, + LoadBalancerProperties loadBalancerProperties, + MetadataLocalProperties metadataLocalProperties, + RouterRuleLabelResolver routerRuleLabelResolver, + List routerLabelResolvers) { + super(clientFactory, gatewayLoadBalancerProperties, loadBalancerProperties); + + this.clientFactory = clientFactory; + this.gatewayLoadBalancerProperties = gatewayLoadBalancerProperties; + this.loadBalancerProperties = loadBalancerProperties; + this.metadataLocalProperties = metadataLocalProperties; + this.routerRuleLabelResolver = routerRuleLabelResolver; + this.routerLabelResolvers = routerLabelResolvers; + } + + /** + * Copied from ReactiveLoadBalancerClientFilter, and create new RequestData for passing router labels. + */ + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); + String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR); + if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) { + return chain.filter(exchange); + } + // preserve the original url + addOriginalRequestUrl(exchange, url); + + if (log.isTraceEnabled()) { + log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url); + } + + URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); + String serviceId = requestUri.getHost(); + Set supportedLifecycleProcessors = LoadBalancerLifecycleValidator + .getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), + RequestDataContext.class, ResponseData.class, ServiceInstance.class); + + // Pass route tags through http headers + HttpHeaders routerHttpHeaders = genRouterHttpHeaders(exchange, serviceId); + + ServerHttpRequest request = exchange.getRequest(); + RequestData requestData = new RequestData(request.getMethod(), request.getURI(), routerHttpHeaders, + new HttpHeaders(), new HashMap<>()); + DefaultRequest lbRequest = new DefaultRequest<>(new RequestDataContext( + requestData, getHint(serviceId, loadBalancerProperties.getHint()))); + + return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> { + + if (!response.hasServer()) { + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle + .onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, response))); + throw NotFoundException.create(gatewayLoadBalancerProperties.isUse404(), + "Unable to find instance for " + url.getHost()); + } + + ServiceInstance retrievedInstance = response.getServer(); + + URI uri = exchange.getRequest().getURI(); + + // if the `lb:` mechanism was used, use `` as the default, + // if the loadbalancer doesn't provide one. + String overrideScheme = retrievedInstance.isSecure() ? "https" : "http"; + if (schemePrefix != null) { + overrideScheme = url.getScheme(); + } + + DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance, + overrideScheme); + + URI requestUrl = reconstructURI(serviceInstance, uri); + + if (log.isTraceEnabled()) { + log.trace("LoadBalancerClientFilter url chosen: " + requestUrl); + } + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); + exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response); + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, response)); + }).then(chain.filter(exchange)) + .doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle + .onComplete(new CompletionContext( + CompletionContext.Status.FAILED, throwable, lbRequest, + exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR))))) + .doOnSuccess(aVoid -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle + .onComplete(new CompletionContext( + CompletionContext.Status.SUCCESS, lbRequest, + exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR), + new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest())))))); + } + + protected URI reconstructURI(ServiceInstance serviceInstance, URI original) { + return LoadBalancerUriTools.reconstructURI(serviceInstance, original); + } + + private Mono> choose(Request lbRequest, String serviceId, + Set supportedLifecycleProcessors) { + ReactorLoadBalancer loadBalancer = this.clientFactory.getInstance(serviceId, + ReactorServiceInstanceLoadBalancer.class); + if (loadBalancer == null) { + throw new NotFoundException("No loadbalancer available for " + serviceId); + } + supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest)); + return loadBalancer.choose(lbRequest); + } + + // no actual used + private String getHint(String serviceId, Map hints) { + String defaultHint = hints.getOrDefault("default", "default"); + String hintPropertyValue = hints.get(serviceId); + return hintPropertyValue != null ? hintPropertyValue : defaultHint; + } + + // In order to be consistent with feign and restTemplate, + // the router label is passed through the http header uniformly instead of the original hint mechanism. + HttpHeaders genRouterHttpHeaders(ServerWebExchange exchange, String peerServiceName) { + HttpHeaders headers = new HttpHeaders(); + headers.add(RouterConstants.ROUTER_LABEL_HEADER, genRouterHint(exchange, peerServiceName)); + return headers; + } + + private String genRouterHint(ServerWebExchange exchange, String peerServiceName) { + Map routerLabels = genRouterLabels(exchange, peerServiceName); + String encodedLabelsContent; + try { + encodedLabelsContent = URLEncoder.encode(JacksonUtils.serialize2Json(routerLabels), StandardCharsets.UTF_8.name()); + } + catch (UnsupportedEncodingException e) { + throw new RuntimeException("unsupported charset exception " + StandardCharsets.UTF_8.name()); + } + return encodedLabelsContent; + } + + private Map genRouterLabels(ServerWebExchange exchange, String peerServiceName) { + // local service labels + Map labels = new HashMap<>(metadataLocalProperties.getContent()); + + // labels from rule expression + Map ruleExpressionLabels = getExpressionLabels(exchange, peerServiceName); + if (!CollectionUtils.isEmpty(ruleExpressionLabels)) { + labels.putAll(ruleExpressionLabels); + } + + // labels from request + if (!CollectionUtils.isEmpty(routerLabelResolvers)) { + routerLabelResolvers.forEach(resolver -> { + try { + Map customResolvedLabels = resolver.resolve(exchange); + if (!CollectionUtils.isEmpty(customResolvedLabels)) { + labels.putAll(customResolvedLabels); + } + } + catch (Throwable t) { + log.error("[SCT][Router] revoke RouterLabelResolver occur some exception. ", t); + } + }); + } + + // labels from downstream + Map transitiveLabels = MetadataContextHolder.get() + .getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE); + labels.putAll(transitiveLabels); + + return labels; + } + + private Map getExpressionLabels(ServerWebExchange exchange, String peerServiceName) { + Set labelKeys = routerRuleLabelResolver.getExpressionLabelKeys(MetadataContext.LOCAL_NAMESPACE, + MetadataContext.LOCAL_SERVICE, peerServiceName); + + if (CollectionUtils.isEmpty(labelKeys)) { + return Collections.emptyMap(); + } + + return ExpressionLabelUtils.resolve(exchange, labelKeys); + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/spi/RouterLabelResolver.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/spi/RouterLabelResolver.java index 99a6b5bf3..d885581ca 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/spi/RouterLabelResolver.java +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/spi/RouterLabelResolver.java @@ -25,6 +25,7 @@ import feign.RequestTemplate; import org.springframework.core.Ordered; import org.springframework.http.HttpRequest; +import org.springframework.web.server.ServerWebExchange; /** * The spi for resolving labels from request. @@ -51,4 +52,13 @@ public interface RouterLabelResolver extends Ordered { default Map resolve(HttpRequest request, byte[] body) { return Collections.emptyMap(); } + + /** + * resolve labels from server web exchange. + * @param exchange the server web exchange. + * @return resolved labels + */ + default Map resolve(ServerWebExchange exchange) { + return Collections.emptyMap(); + } } diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/spring.factories b/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/spring.factories index d33dcea7f..3b20deb56 100644 --- a/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-starter-tencent-polaris-router/src/main/resources/META-INF/spring.factories @@ -1,2 +1,3 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - com.tencent.cloud.polaris.router.config.RouterAutoConfiguration + com.tencent.cloud.polaris.router.config.RouterAutoConfiguration,\ + com.tencent.cloud.polaris.router.config.FeignAutoConfiguration diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessorTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessorTest.java new file mode 100644 index 000000000..35e91ca73 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisLoadBalancerClientBeanPostProcessorTest.java @@ -0,0 +1,100 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.polaris.router.scg; + +import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; +import com.tencent.cloud.common.util.BeanFactoryUtils; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.RouterLabelResolver; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties; +import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; + +import static org.mockito.Mockito.when; + + +/** + * Test for ${@link PolarisLoadBalancerClientBeanPostProcessor} + *@author lepdou 2022-07-04 + */ +@RunWith(MockitoJUnitRunner.class) +public class PolarisLoadBalancerClientBeanPostProcessorTest { + + @Mock + private BeanFactory beanFactory; + @Mock + private LoadBalancerClientFactory loadBalancerClientFactory; + @Mock + private GatewayLoadBalancerProperties gatewayLoadBalancerProperties; + @Mock + private LoadBalancerProperties loadBalancerProperties; + @Mock + private MetadataLocalProperties metadataLocalProperties; + @Mock + private RouterRuleLabelResolver routerRuleLabelResolver; + + @Test + public void testWrapReactiveLoadBalancerClientFilter() { + when(beanFactory.getBean(LoadBalancerClientFactory.class)).thenReturn(loadBalancerClientFactory); + when(beanFactory.getBean(GatewayLoadBalancerProperties.class)).thenReturn(gatewayLoadBalancerProperties); + when(beanFactory.getBean(LoadBalancerProperties.class)).thenReturn(loadBalancerProperties); + when(beanFactory.getBean(MetadataLocalProperties.class)).thenReturn(metadataLocalProperties); + when(beanFactory.getBean(RouterRuleLabelResolver.class)).thenReturn(routerRuleLabelResolver); + + try (MockedStatic mockedBeanFactoryUtils = Mockito.mockStatic(BeanFactoryUtils.class)) { + mockedBeanFactoryUtils.when(() -> BeanFactoryUtils.getBeans(beanFactory, RouterLabelResolver.class)) + .thenReturn(null); + + ReactiveLoadBalancerClientFilter reactiveLoadBalancerClientFilter = new ReactiveLoadBalancerClientFilter( + loadBalancerClientFactory, gatewayLoadBalancerProperties, loadBalancerProperties); + + PolarisLoadBalancerClientBeanPostProcessor processor = new PolarisLoadBalancerClientBeanPostProcessor(); + processor.setBeanFactory(beanFactory); + + Object bean = processor.postProcessBeforeInitialization(reactiveLoadBalancerClientFilter, ""); + + Assert.assertTrue(bean instanceof PolarisReactiveLoadBalancerClientFilter); + } + } + + @Test + public void testNotWrapLoadBalancerInterceptor() { + PolarisLoadBalancerClientBeanPostProcessor processor = new PolarisLoadBalancerClientBeanPostProcessor(); + processor.setBeanFactory(beanFactory); + + OtherBean otherBean = new OtherBean(); + Object bean = processor.postProcessBeforeInitialization(otherBean, ""); + Assert.assertFalse(bean instanceof PolarisReactiveLoadBalancerClientFilter); + Assert.assertTrue(bean instanceof OtherBean); + } + + static class OtherBean { + + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilterTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilterTest.java new file mode 100644 index 000000000..1ed4813fd --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/scg/PolarisReactiveLoadBalancerClientFilterTest.java @@ -0,0 +1,145 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.polaris.router.scg; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.config.MetadataLocalProperties; +import com.tencent.cloud.common.util.ApplicationContextAwareUtils; +import com.tencent.cloud.common.util.JacksonUtils; +import com.tencent.cloud.polaris.router.RouterConstants; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.RouterLabelResolver; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.util.CollectionUtils; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * Test for ${@link PolarisReactiveLoadBalancerClientFilter} + *@author lepdou 2022-07-04 + */ +@RunWith(MockitoJUnitRunner.class) +public class PolarisReactiveLoadBalancerClientFilterTest { + + private static final String callerService = "callerService"; + private static final String calleeService = "calleeService"; + private static MockedStatic mockedApplicationContextAwareUtils; + private static MockedStatic mockedMetadataContextHolder; + + @Mock + private MetadataLocalProperties metadataLocalProperties; + @Mock + private RouterLabelResolver routerLabelResolver; + @Mock + private RouterRuleLabelResolver routerRuleLabelResolver; + @Mock + private LoadBalancerClientFactory loadBalancerClientFactory; + @Mock + private GatewayLoadBalancerProperties gatewayLoadBalancerProperties; + @Mock + private LoadBalancerProperties loadBalancerProperties; + + @BeforeClass + public static void beforeClass() { + mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); + mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) + .thenReturn(callerService); + + MetadataContext metadataContext = Mockito.mock(MetadataContext.class); + + // mock transitive metadata + Map transitiveLabels = new HashMap<>(); + transitiveLabels.put("t1", "v1"); + transitiveLabels.put("t2", "v2"); + when(metadataContext.getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE)).thenReturn(transitiveLabels); + + mockedMetadataContextHolder = Mockito.mockStatic(MetadataContextHolder.class); + mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext); + } + + @AfterClass + public static void afterClass() { + mockedApplicationContextAwareUtils.close(); + mockedMetadataContextHolder.close(); + } + + @Test + public void testGenRouterHttpHeaders() throws UnsupportedEncodingException { + PolarisReactiveLoadBalancerClientFilter filter = new PolarisReactiveLoadBalancerClientFilter(loadBalancerClientFactory, + gatewayLoadBalancerProperties, loadBalancerProperties, metadataLocalProperties, routerRuleLabelResolver, + Lists.newArrayList(routerLabelResolver)); + + Map localMetadata = new HashMap<>(); + localMetadata.put("env", "blue"); + when(metadataLocalProperties.getContent()).thenReturn(localMetadata); + + Set expressionLabelKeys = Sets.newHashSet("${http.header.k1}", "${http.query.userid}"); + when(routerRuleLabelResolver.getExpressionLabelKeys(anyString(), anyString(), anyString())).thenReturn(expressionLabelKeys); + + MockServerHttpRequest request = MockServerHttpRequest.get("/" + calleeService + "/users") + .header("k1", "v1") + .queryParam("userid", "zhangsan") + .build(); + MockServerWebExchange webExchange = new MockServerWebExchange.Builder(request).build(); + + Map customMetadata = new HashMap<>(); + customMetadata.put("k2", "v2"); + when(routerLabelResolver.resolve(webExchange)).thenReturn(customMetadata); + + HttpHeaders headers = filter.genRouterHttpHeaders(webExchange, calleeService); + + Assert.assertNotNull(headers); + List routerHeaders = headers.get(RouterConstants.ROUTER_LABEL_HEADER); + Assert.assertFalse(CollectionUtils.isEmpty(routerHeaders)); + + Map routerLabels = JacksonUtils.deserialize2Map(URLDecoder.decode(routerHeaders.get(0), StandardCharsets.UTF_8.name())); + Assert.assertEquals("v1", routerLabels.get("${http.header.k1}")); + Assert.assertEquals("zhangsan", routerLabels.get("${http.query.userid}")); + Assert.assertEquals("blue", routerLabels.get("env")); + Assert.assertEquals("v1", routerLabels.get("t1")); + Assert.assertEquals("v2", routerLabels.get("t2")); + } +} diff --git a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/pom.xml b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/pom.xml new file mode 100644 index 000000000..842363af9 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/pom.xml @@ -0,0 +1,28 @@ + + + + polaris-gateway-example + com.tencent.cloud + ${revision} + ../pom.xml + + 4.0.0 + + gateway-callee-service2 + Spring Cloud Starter Tencent Polaris Gateway Callee Example + + + + com.tencent.cloud + spring-cloud-starter-tencent-polaris-discovery + + + + org.springframework.boot + spring-boot-starter-web + + + + diff --git a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeApplication.java b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeApplication.java new file mode 100644 index 000000000..4e5e816bd --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeApplication.java @@ -0,0 +1,35 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.gateway.example.callee; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Gateway callee application. + * + * @author Haotian Zhang + */ +@SpringBootApplication +public class GatewayCalleeApplication { + + public static void main(String[] args) { + SpringApplication.run(GatewayCalleeApplication.class, args); + } + +} diff --git a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeController.java b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeController.java new file mode 100644 index 000000000..eae571921 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/java/com/tencent/cloud/polaris/gateway/example/callee/GatewayCalleeController.java @@ -0,0 +1,71 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package com.tencent.cloud.polaris.gateway.example.callee; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + +import com.tencent.cloud.common.constant.MetadataConstant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Gateway callee controller. + * + * @author Haotian Zhang + */ +@RestController +@RequestMapping("/gateway/example/callee") +public class GatewayCalleeController { + + private static Logger LOG = LoggerFactory.getLogger(GatewayCalleeController.class); + + @Value("${server.port:0}") + private int port; + + /** + * Get information of callee. + * @return information of callee + */ + @RequestMapping("/info") + public String info() { + LOG.info("Gateway Example Callee [{}] is called.", port); + return String.format("Gateway Example Callee [%s] is called.", port); + } + + /** + * Get metadata in HTTP header. + * + * @param metadataStr metadata string + * @return metadata in HTTP header + * @throws UnsupportedEncodingException encoding exception + */ + @RequestMapping("/echo") + public String echoHeader(@RequestHeader(MetadataConstant.HeaderName.CUSTOM_METADATA) String metadataStr) + throws UnsupportedEncodingException { + LOG.info(URLDecoder.decode(metadataStr, StandardCharsets.UTF_8.name())); + return URLDecoder.decode(metadataStr, StandardCharsets.UTF_8.name()); + } + +} diff --git a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/resources/bootstrap.yml b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/resources/bootstrap.yml new file mode 100644 index 000000000..f124251c8 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-callee-service2/src/main/resources/bootstrap.yml @@ -0,0 +1,10 @@ +server: + session-timeout: 1800 + port: 48082 +spring: + application: + name: GatewayCalleeService + cloud: + polaris: + address: grpc://183.47.111.80:8091 + namespace: default diff --git a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/pom.xml b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/pom.xml index 8b8b50bf6..b6643b49d 100644 --- a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/pom.xml +++ b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/pom.xml @@ -26,7 +26,7 @@ com.tencent.cloud - spring-cloud-starter-tencent-metadata-transfer + spring-cloud-starter-tencent-polaris-router @@ -39,4 +39,4 @@ spring-cloud-loadbalancer - \ No newline at end of file + diff --git a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/src/main/resources/bootstrap.yml b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/src/main/resources/bootstrap.yml index 5d949573b..55f03dfa3 100644 --- a/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/src/main/resources/bootstrap.yml +++ b/spring-cloud-tencent-examples/polaris-gateway-example/gateway-scg-service/src/main/resources/bootstrap.yml @@ -9,14 +9,10 @@ spring: metadata: content: a: 1 - transitive: - - a polaris: address: grpc://183.47.111.80:8091 namespace: default enabled: true - discovery: - service-list-refresh-interval: 1000 gateway: discovery: locator: diff --git a/spring-cloud-tencent-examples/polaris-gateway-example/pom.xml b/spring-cloud-tencent-examples/polaris-gateway-example/pom.xml index 2607823df..e67b8a8dd 100644 --- a/spring-cloud-tencent-examples/polaris-gateway-example/pom.xml +++ b/spring-cloud-tencent-examples/polaris-gateway-example/pom.xml @@ -17,6 +17,7 @@ gateway-scg-service gateway-callee-service + gateway-callee-service2 @@ -34,4 +35,4 @@ - \ No newline at end of file + diff --git a/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/src/main/java/com/tencent/cloud/polaris/router/example/CustomRouterLabelResolver.java b/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/src/main/java/com/tencent/cloud/polaris/router/example/CustomRouterLabelResolver.java index 815b2baff..bd289abb9 100644 --- a/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/src/main/java/com/tencent/cloud/polaris/router/example/CustomRouterLabelResolver.java +++ b/spring-cloud-tencent-examples/polaris-router-example/router-caller-service/src/main/java/com/tencent/cloud/polaris/router/example/CustomRouterLabelResolver.java @@ -58,7 +58,6 @@ public class CustomRouterLabelResolver implements RouterLabelResolver { return labels; } - @Override public int getOrder() { return 0; diff --git a/spring-cloud-tencent-polaris-loadbalancer/pom.xml b/spring-cloud-tencent-polaris-loadbalancer/pom.xml index cfec39a05..da560c430 100644 --- a/spring-cloud-tencent-polaris-loadbalancer/pom.xml +++ b/spring-cloud-tencent-polaris-loadbalancer/pom.xml @@ -49,6 +49,24 @@ spring-boot-starter-test test + + + org.mockito + mockito-inline + test + + + + org.mockito + mockito-core + test + + + + net.bytebuddy + byte-buddy + test + - \ No newline at end of file + diff --git a/spring-cloud-tencent-polaris-loadbalancer/src/main/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtils.java b/spring-cloud-tencent-polaris-loadbalancer/src/main/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtils.java index 0a6816ba0..47217fd93 100644 --- a/spring-cloud-tencent-polaris-loadbalancer/src/main/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtils.java +++ b/spring-cloud-tencent-polaris-loadbalancer/src/main/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtils.java @@ -18,7 +18,9 @@ package com.tencent.cloud.polaris.loadbalancer; +import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import com.tencent.cloud.common.metadata.MetadataContext; @@ -40,26 +42,32 @@ import org.springframework.util.CollectionUtils; public class LoadBalancerUtils { public static ServiceInstances transferServersToServiceInstances(Flux> servers) { - List instances = servers.toStream().flatMap(List::stream).map(serviceInstance -> { - DefaultInstance instance = new DefaultInstance(); - instance.setNamespace(MetadataContext.LOCAL_NAMESPACE); - instance.setService(serviceInstance.getServiceId()); - instance.setProtocol(serviceInstance.getScheme()); - instance.setId(serviceInstance.getInstanceId()); - instance.setHost(serviceInstance.getHost()); - instance.setPort(serviceInstance.getPort()); - instance.setWeight(100); - instance.setMetadata(serviceInstance.getMetadata()); - return instance; - }).collect(Collectors.toList()); + AtomicReference> instances = new AtomicReference<>(); + servers.subscribe(serviceInstances -> { + instances.set(serviceInstances.stream().map(serviceInstance -> { + DefaultInstance instance = new DefaultInstance(); + instance.setNamespace(MetadataContext.LOCAL_NAMESPACE); + instance.setService(serviceInstance.getServiceId()); + instance.setProtocol(serviceInstance.getScheme()); + instance.setId(serviceInstance.getInstanceId()); + instance.setHost(serviceInstance.getHost()); + instance.setPort(serviceInstance.getPort()); + instance.setWeight(100); + instance.setMetadata(serviceInstance.getMetadata()); + return instance; + }).collect(Collectors.toList())); + }); String serviceName = null; - if (!CollectionUtils.isEmpty(instances)) { - serviceName = instances.get(0).getService(); + if (CollectionUtils.isEmpty(instances.get())) { + instances.set(Collections.emptyList()); + } + else { + serviceName = instances.get().get(0).getService(); } ServiceKey serviceKey = new ServiceKey(MetadataContext.LOCAL_NAMESPACE, serviceName); - return new DefaultServiceInstances(serviceKey, instances); + return new DefaultServiceInstances(serviceKey, instances.get()); } } diff --git a/spring-cloud-tencent-polaris-loadbalancer/src/test/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtilsTest.java b/spring-cloud-tencent-polaris-loadbalancer/src/test/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtilsTest.java new file mode 100644 index 000000000..28b62fdd6 --- /dev/null +++ b/spring-cloud-tencent-polaris-loadbalancer/src/test/java/com/tencent/cloud/polaris/loadbalancer/LoadBalancerUtilsTest.java @@ -0,0 +1,106 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + +package com.tencent.cloud.polaris.loadbalancer; + +import java.util.ArrayList; +import java.util.List; + +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.util.ApplicationContextAwareUtils; +import com.tencent.polaris.api.pojo.Instance; +import com.tencent.polaris.api.pojo.ServiceInstances; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; + +import static org.mockito.ArgumentMatchers.anyString; + +/** + * Test for ${@link LoadBalancerUtils}. + *@author lepdou 2022-07-04 + */ +@RunWith(MockitoJUnitRunner.class) +public class LoadBalancerUtilsTest { + + private static MockedStatic mockedApplicationContextAwareUtils; + private static MockedStatic mockedMetadataContextHolder; + + private static final String testNamespaceAndService = "testNamespaceAndService"; + + @BeforeClass + public static void beforeClass() { + mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); + mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) + .thenReturn(testNamespaceAndService); + + MetadataContext metadataContext = Mockito.mock(MetadataContext.class); + mockedMetadataContextHolder = Mockito.mockStatic(MetadataContextHolder.class); + mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext); + } + + @AfterClass + public static void afterClass() { + mockedApplicationContextAwareUtils.close(); + mockedMetadataContextHolder.close(); + } + + @Test + public void testTransferEmptyInstances() { + ServiceInstances serviceInstances = LoadBalancerUtils.transferServersToServiceInstances(Flux.empty()); + Assert.assertNotNull(serviceInstances.getInstances()); + Assert.assertEquals(0, serviceInstances.getInstances().size()); + } + + @Test + public void testTransferNotEmptyInstances() { + int instanceSize = 100; + + List instances = new ArrayList<>(); + for (int i = 0; i < instanceSize; i++) { + instances.add(new DefaultServiceInstance("ins" + i, testNamespaceAndService, "127.0.0." + i, + 8080, false)); + } + + ServiceInstances serviceInstances = LoadBalancerUtils.transferServersToServiceInstances(Flux.just(instances)); + + Assert.assertNotNull(serviceInstances.getInstances()); + Assert.assertEquals(instanceSize, serviceInstances.getInstances().size()); + + List polarisInstances = serviceInstances.getInstances(); + for (int i = 0; i < instanceSize; i++) { + Instance instance = polarisInstances.get(i); + Assert.assertEquals(testNamespaceAndService, instance.getNamespace()); + Assert.assertEquals(testNamespaceAndService, instance.getService()); + Assert.assertEquals("ins" + i, instance.getId()); + Assert.assertEquals("127.0.0." + i, instance.getHost()); + Assert.assertEquals(8080, instance.getPort()); + Assert.assertEquals(100, instance.getWeight()); + } + } +}