diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c1f8175..1852bce3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ # Change Log --- +- [Fix issue 593:When the project depends on spring-retry, executing restTemplate to call microservice and reporting NPE error](https://github.com/Tencent/spring-cloud-tencent/pull/594) 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 5c8a13286..4b60b75b4 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,6 +18,11 @@ package com.tencent.cloud.polaris.router.config; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.tencent.cloud.common.metadata.StaticMetadataManager; import com.tencent.cloud.polaris.context.ServiceRuleManager; import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; import com.tencent.cloud.polaris.router.beanprocessor.LoadBalancerInterceptorBeanPostProcessor; @@ -28,7 +33,11 @@ import com.tencent.cloud.polaris.router.config.properties.PolarisRuleBasedRouter import com.tencent.cloud.polaris.router.interceptor.MetadataRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.NearbyRouterRequestInterceptor; import com.tencent.cloud.polaris.router.interceptor.RuleBasedRouterRequestInterceptor; +import com.tencent.cloud.polaris.router.resttemplate.RouterLabelRestTemplateInterceptor; +import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; @@ -36,6 +45,8 @@ 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.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE; @@ -85,4 +96,34 @@ public class RouterAutoConfiguration { public RuleBasedRouterRequestInterceptor ruleBasedRouterRequestInterceptor(PolarisRuleBasedRouterProperties polarisRuleBasedRouterProperties) { return new RuleBasedRouterRequestInterceptor(polarisRuleBasedRouterProperties); } + + /** + * Create when RestTemplate exists. + * @author liuye 2022-09-14 + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "org.springframework.web.client.RestTemplate") + @ConditionalOnProperty(value = "spring.cloud.polaris.router.rule-router.enabled", matchIfMissing = true) + protected static class RouterLabelRestTemplateConfig { + + @Autowired(required = false) + private List restTemplates = Collections.emptyList(); + + @Bean + public RouterLabelRestTemplateInterceptor routerLabelRestTemplateInterceptor( + List routerLabelResolvers, + StaticMetadataManager staticMetadataManager, + RouterRuleLabelResolver routerRuleLabelResolver) { + return new RouterLabelRestTemplateInterceptor(routerLabelResolvers, staticMetadataManager, routerRuleLabelResolver); + } + + @Bean + public SmartInitializingSingleton addRouterLabelInterceptorForRestTemplate(RouterLabelRestTemplateInterceptor interceptor) { + return () -> restTemplates.forEach(restTemplate -> { + List list = new ArrayList<>(restTemplate.getInterceptors()); + list.add(interceptor); + restTemplate.setInterceptors(list); + }); + } + } } diff --git a/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterLabelRestTemplateInterceptor.java b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterLabelRestTemplateInterceptor.java new file mode 100644 index 000000000..8e13ee0ee --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/main/java/com/tencent/cloud/polaris/router/resttemplate/RouterLabelRestTemplateInterceptor.java @@ -0,0 +1,151 @@ +/* + * 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.resttemplate; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.tencent.cloud.common.constant.RouterConstants; +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.StaticMetadataManager; +import com.tencent.cloud.common.util.JacksonUtils; +import com.tencent.cloud.common.util.expresstion.SpringWebExpressionLabelUtils; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.core.Ordered; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import static com.tencent.cloud.common.constant.ContextConstant.UTF_8; + +/** + * Interceptor used for adding the route label in http headers from context when web client + * is RestTemplate. + * + * @author liuye 2022-09-14 + */ +public class RouterLabelRestTemplateInterceptor implements ClientHttpRequestInterceptor, Ordered { + private static final Logger LOGGER = LoggerFactory.getLogger(RouterLabelRestTemplateInterceptor.class); + + private final List routerLabelResolvers; + private final StaticMetadataManager staticMetadataManager; + private final RouterRuleLabelResolver routerRuleLabelResolver; + + public RouterLabelRestTemplateInterceptor(List routerLabelResolvers, + StaticMetadataManager staticMetadataManager, + RouterRuleLabelResolver routerRuleLabelResolver) { + this.staticMetadataManager = staticMetadataManager; + this.routerRuleLabelResolver = routerRuleLabelResolver; + + if (!CollectionUtils.isEmpty(routerLabelResolvers)) { + routerLabelResolvers.sort(Comparator.comparingInt(Ordered::getOrder)); + this.routerLabelResolvers = routerLabelResolvers; + } + else { + this.routerLabelResolvers = null; + } + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @Override + public ClientHttpResponse intercept(@NonNull HttpRequest request, @NonNull byte[] body, + @NonNull ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { + final URI originalUri = request.getURI(); + String peerServiceName = originalUri.getHost(); + Assert.state(peerServiceName != null, + "Request URI does not contain a valid hostname: " + originalUri); + + setLabelsToHeaders(request, body, peerServiceName); + + return clientHttpRequestExecution.execute(request, body); + } + + void setLabelsToHeaders(HttpRequest request, byte[] body, String peerServiceName) { + // local service labels + Map labels = new HashMap<>(staticMetadataManager.getMergedStaticMetadata()); + + // labels from rule expression + Set expressionLabelKeys = routerRuleLabelResolver.getExpressionLabelKeys(MetadataContext.LOCAL_NAMESPACE, + MetadataContext.LOCAL_SERVICE, peerServiceName); + + Map ruleExpressionLabels = getExpressionLabels(request, expressionLabelKeys); + if (!CollectionUtils.isEmpty(ruleExpressionLabels)) { + labels.putAll(ruleExpressionLabels); + } + + // labels from request + if (!CollectionUtils.isEmpty(routerLabelResolvers)) { + routerLabelResolvers.forEach(resolver -> { + try { + Map customResolvedLabels = resolver.resolve(request, body, expressionLabelKeys); + if (!CollectionUtils.isEmpty(customResolvedLabels)) { + labels.putAll(customResolvedLabels); + } + } + catch (Throwable t) { + LOGGER.error("[SCT][Router] revoke RouterLabelResolver occur some exception. ", t); + } + }); + } + + // labels from downstream + Map transitiveLabels = MetadataContextHolder.get() + .getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE); + labels.putAll(transitiveLabels); + + // pass label by header + String encodedLabelsContent; + try { + encodedLabelsContent = URLEncoder.encode(JacksonUtils.serialize2Json(labels), UTF_8); + } + catch (UnsupportedEncodingException e) { + throw new RuntimeException("unsupported charset exception " + UTF_8); + } + request.getHeaders().set(RouterConstants.ROUTER_LABEL_HEADER, encodedLabelsContent); + } + + private Map getExpressionLabels(HttpRequest request, Set labelKeys) { + if (CollectionUtils.isEmpty(labelKeys)) { + return Collections.emptyMap(); + } + + return SpringWebExpressionLabelUtils.resolve(request, labelKeys); + } +} diff --git a/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/RouterLabelRestTemplateInterceptorTest.java b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/RouterLabelRestTemplateInterceptorTest.java new file mode 100644 index 000000000..4539ab45b --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-router/src/test/java/com/tencent/cloud/polaris/router/resttemplate/RouterLabelRestTemplateInterceptorTest.java @@ -0,0 +1,166 @@ +/* + * 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.resttemplate; + + +import java.net.URI; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.tencent.cloud.common.constant.RouterConstants; +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import com.tencent.cloud.common.metadata.StaticMetadataManager; +import com.tencent.cloud.common.util.ApplicationContextAwareUtils; +import com.tencent.cloud.common.util.JacksonUtils; +import com.tencent.cloud.polaris.router.RouterRuleLabelResolver; +import com.tencent.cloud.polaris.router.spi.SpringWebRouterLabelResolver; +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.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * test for {@link RouterLabelRestTemplateInterceptor} + * @author liuye 2022-09-16 + */ +@RunWith(MockitoJUnitRunner.class) +public class RouterLabelRestTemplateInterceptorTest { + + private static MockedStatic mockedApplicationContextAwareUtils; + private static MockedStatic mockedMetadataContextHolder; + @Mock + private SpringWebRouterLabelResolver routerLabelResolver; + @Mock + private StaticMetadataManager staticMetadataManager; + @Mock + private RouterRuleLabelResolver routerRuleLabelResolver; + + @BeforeClass + public static void beforeClass() { + mockedApplicationContextAwareUtils = Mockito.mockStatic(ApplicationContextAwareUtils.class); + mockedApplicationContextAwareUtils.when(() -> ApplicationContextAwareUtils.getProperties(anyString())) + .thenReturn("callerService"); + + mockedMetadataContextHolder = Mockito.mockStatic(MetadataContextHolder.class); + } + + @AfterClass + public static void afterClass() { + mockedApplicationContextAwareUtils.close(); + mockedMetadataContextHolder.close(); + } + + @Test + public void testRouterContext() throws Exception { + String callerService = "callerService"; + String calleeService = "calleeService"; + HttpRequest request = new MockedHttpRequest("http://" + calleeService + "/user/get"); + + // mock local metadata + Map localMetadata = new HashMap<>(); + localMetadata.put("k1", "v1"); + localMetadata.put("k2", "v2"); + when(staticMetadataManager.getMergedStaticMetadata()).thenReturn(localMetadata); + + // mock expression rule labels + + Set expressionKeys = new HashSet<>(); + expressionKeys.add("${http.method}"); + expressionKeys.add("${http.uri}"); + when(routerRuleLabelResolver.getExpressionLabelKeys(callerService, callerService, calleeService)).thenReturn(expressionKeys); + + // mock custom resolved from request + Map customResolvedLabels = new HashMap<>(); + customResolvedLabels.put("k2", "v22"); + customResolvedLabels.put("k4", "v4"); + when(routerLabelResolver.resolve(request, null, expressionKeys)).thenReturn(customResolvedLabels); + + MetadataContext metadataContext = Mockito.mock(MetadataContext.class); + + // mock transitive metadata + Map transitiveLabels = new HashMap<>(); + transitiveLabels.put("k1", "v1"); + transitiveLabels.put("k2", "v22"); + when(metadataContext.getFragmentContext(MetadataContext.FRAGMENT_TRANSITIVE)).thenReturn(transitiveLabels); + + mockedMetadataContextHolder.when(MetadataContextHolder::get).thenReturn(metadataContext); + + RouterLabelRestTemplateInterceptor routerLabelRestTemplateInterceptor = new RouterLabelRestTemplateInterceptor( + Collections.singletonList(routerLabelResolver), staticMetadataManager, routerRuleLabelResolver); + + routerLabelRestTemplateInterceptor.setLabelsToHeaders(request, null, calleeService); + + verify(staticMetadataManager).getMergedStaticMetadata(); + verify(routerRuleLabelResolver).getExpressionLabelKeys(callerService, callerService, calleeService); + verify(routerLabelResolver).resolve(request, null, expressionKeys); + + + Map headers = JacksonUtils.deserialize2Map(URLDecoder.decode(request.getHeaders() + .get(RouterConstants.ROUTER_LABEL_HEADER).get(0), "UTF-8")); + Assert.assertEquals("v1", headers.get("k1")); + Assert.assertEquals("v22", headers.get("k2")); + Assert.assertEquals("v4", headers.get("k4")); + Assert.assertEquals("GET", headers.get("${http.method}")); + Assert.assertEquals("/user/get", headers.get("${http.uri}")); + } + + static class MockedHttpRequest implements HttpRequest { + + private URI uri; + + private HttpHeaders httpHeaders = new HttpHeaders(); + + MockedHttpRequest(String url) { + this.uri = URI.create(url); + } + + @Override + public String getMethodValue() { + return HttpMethod.GET.name(); + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public HttpHeaders getHeaders() { + return httpHeaders; + } + } +}