support spring cloud gateway routers (#390)

pull/403/head
lepdou 3 years ago committed by GitHub
parent 55c1d29225
commit 1ce6336f51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -26,3 +26,4 @@
- [Feature: support actuator for sct core components](https://github.com/Tencent/spring-cloud-tencent/pull/369)
- [docs: Fix javadoc <br /> error](https://github.com/Tencent/spring-cloud-tencent/pull/376)
- [docs:optimize example](https://github.com/Tencent/spring-cloud-tencent/pull/387)
- [Feature: support spring cloud gateway routers](https://github.com/Tencent/spring-cloud-tencent/pull/390)

@ -52,6 +52,20 @@
<optional>true</optional>
</dependency>
<!-- spring cloud gateway dependencies start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<optional>true</optional>
</dependency>
<!-- spring cloud gateway dependencies end -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

@ -0,0 +1,48 @@
/*
* 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;
/**
* Auto configuration for feign components.
* @author lepdou 2022-07-04
*/
@Configuration
@ConditionalOnClass(name = {"feign.RequestInterceptor"})
public class FeignAutoConfiguration {
@Bean
public RouterLabelFeignInterceptor routerLabelInterceptor(@Nullable List<RouterLabelResolver> routerLabelResolvers,
MetadataLocalProperties metadataLocalProperties,
RouterRuleLabelResolver routerRuleLabelResolver) {
return new RouterLabelFeignInterceptor(routerLabelResolvers, metadataLocalProperties, routerRuleLabelResolver);
}
}

@ -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<RouterLabelResolver> 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

@ -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<RouterLabelResolver> 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;
}
}

@ -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<RouterLabelResolver> routerLabelResolvers;
public PolarisReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,
GatewayLoadBalancerProperties gatewayLoadBalancerProperties,
LoadBalancerProperties loadBalancerProperties,
MetadataLocalProperties metadataLocalProperties,
RouterRuleLabelResolver routerRuleLabelResolver,
List<RouterLabelResolver> 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<Void> 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<LoadBalancerLifecycle> 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<RequestDataContext> 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:<scheme>` mechanism was used, use `<scheme>` 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<ResponseData, ServiceInstance, RequestDataContext>(
CompletionContext.Status.FAILED, throwable, lbRequest,
exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR)))))
.doOnSuccess(aVoid -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
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<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
ReactorLoadBalancer<ServiceInstance> 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<String, String> 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;
}
String genRouterHint(ServerWebExchange exchange, String peerServiceName) {
Map<String, String> 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;
}
Map<String, String> genRouterLabels(ServerWebExchange exchange, String peerServiceName) {
// local service labels
Map<String, String> labels = new HashMap<>(metadataLocalProperties.getContent());
// labels from rule expression
Map<String, String> ruleExpressionLabels = getExpressionLabels(exchange, peerServiceName);
if (!CollectionUtils.isEmpty(ruleExpressionLabels)) {
labels.putAll(ruleExpressionLabels);
}
// labels from request
if (!CollectionUtils.isEmpty(routerLabelResolvers)) {
routerLabelResolvers.forEach(resolver -> {
try {
Map<String, String> 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<String, String> transitiveLabels = MetadataContextHolder.get()
.getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE);
labels.putAll(transitiveLabels);
return labels;
}
private Map<String, String> getExpressionLabels(ServerWebExchange exchange, String peerServiceName) {
Set<String> labelKeys = routerRuleLabelResolver.getExpressionLabelKeys(MetadataContext.LOCAL_NAMESPACE,
MetadataContext.LOCAL_SERVICE, peerServiceName);
if (CollectionUtils.isEmpty(labelKeys)) {
return Collections.emptyMap();
}
return ExpressionLabelUtils.resolve(exchange, labelKeys);
}
}

@ -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<String, String> 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<String, String> resolve(ServerWebExchange exchange) {
return Collections.emptyMap();
}
}

@ -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

@ -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<BeanFactoryUtils> 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 {
}
}

@ -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<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
private static MockedStatic<MetadataContextHolder> 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<String, String> 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<String, String> localMetadata = new HashMap<>();
localMetadata.put("env", "blue");
when(metadataLocalProperties.getContent()).thenReturn(localMetadata);
Set<String> 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<String, String> customMetadata = new HashMap<>();
customMetadata.put("k2", "v2");
when(routerLabelResolver.resolve(webExchange)).thenReturn(customMetadata);
HttpHeaders headers = filter.genRouterHttpHeaders(webExchange, calleeService);
Assert.assertNotNull(headers);
List<String> routerHeaders = headers.get(RouterConstants.ROUTER_LABEL_HEADER);
Assert.assertFalse(CollectionUtils.isEmpty(routerHeaders));
Map<String, String> 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"));
}
}

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>polaris-gateway-example</artifactId>
<groupId>com.tencent.cloud</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway-callee-service2</artifactId>
<name>Spring Cloud Starter Tencent Polaris Gateway Callee Example</name>
<dependencies>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-polaris-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

@ -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);
}
}

@ -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());
}
}

@ -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

@ -26,7 +26,7 @@
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-metadata-transfer</artifactId>
<artifactId>spring-cloud-starter-tencent-polaris-router</artifactId>
</dependency>
<dependency>
@ -39,4 +39,4 @@
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
</dependencies>
</project>
</project>

@ -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:

@ -17,6 +17,7 @@
<modules>
<module>gateway-scg-service</module>
<module>gateway-callee-service</module>
<module>gateway-callee-service2</module>
</modules>
<build>
@ -34,4 +35,4 @@
</plugin>
</plugins>
</build>
</project>
</project>

@ -49,6 +49,24 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>

@ -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<List<ServiceInstance>> servers) {
List<Instance> 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<List<Instance>> 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());
}
}

@ -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<ApplicationContextAwareUtils> mockedApplicationContextAwareUtils;
private static MockedStatic<MetadataContextHolder> 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<ServiceInstance> 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<Instance> 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());
}
}
}
Loading…
Cancel
Save