feat: support spring-retry and feign config refresh and feign eager load support schema (#1650)

2022
shedfreewu 2 months ago committed by GitHub
parent 726ce70a2d
commit 4894ab83c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,3 +3,4 @@
- [fix:fix PolarisContextProperties instantiated twice causing NPE.](https://github.com/Tencent/spring-cloud-tencent/pull/1640)
- [fix: fix ConfigChangeListener and unit test](https://github.com/Tencent/spring-cloud-tencent/pull/1655)
- [feat: support spring-retry and feign config refresh and feign eager load support schema](https://github.com/Tencent/spring-cloud-tencent/pull/1650)

@ -1,68 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.circuitbreaker.beanprocessor;
import com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate.PolarisLoadBalancerInterceptor;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
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.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
/**
* LoadBalancerInterceptorBeanPostProcessor is used to wrap the default LoadBalancerInterceptor implementation and returns a custom PolarisLoadBalancerInterceptor.
*
* @author Shedfree Wu
*/
public class LoadBalancerInterceptorBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware, Ordered {
/**
* The order of the bean post processor. if user want to wrap it(CustomLoadBalancerInterceptor -> PolarisLoadBalancerInterceptor), CustomLoadBalancerInterceptorBeanPostProcessor's order should be bigger than ${@link POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER}.
*/
public static final int POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER = 0;
private BeanFactory factory;
@Override
public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
this.factory = beanFactory;
}
@Override
public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
if (bean instanceof LoadBalancerInterceptor) {
// Support rest template router.
// Replaces the default LoadBalancerInterceptor implementation and returns a custom PolarisLoadBalancerInterceptor
LoadBalancerRequestFactory requestFactory = this.factory.getBean(LoadBalancerRequestFactory.class);
LoadBalancerClient loadBalancerClient = this.factory.getBean(LoadBalancerClient.class);
EnhancedPluginRunner pluginRunner = this.factory.getBean(EnhancedPluginRunner.class);
return new PolarisLoadBalancerInterceptor(loadBalancerClient, requestFactory, pluginRunner);
}
return bean;
}
@Override
public int getOrder() {
return POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER;
}
}

@ -21,7 +21,6 @@ import java.util.ArrayList;
import java.util.List;
import com.tencent.cloud.polaris.circuitbreaker.PolarisCircuitBreakerFactory;
import com.tencent.cloud.polaris.circuitbreaker.beanprocessor.LoadBalancerInterceptorBeanPostProcessor;
import com.tencent.cloud.polaris.circuitbreaker.common.CircuitBreakerConfigModifier;
import com.tencent.cloud.polaris.circuitbreaker.reporter.CircuitBreakerPlugin;
import com.tencent.cloud.polaris.circuitbreaker.reporter.ExceptionCircuitBreakerReporter;
@ -32,7 +31,6 @@ import com.tencent.cloud.rpc.enhancement.config.RpcEnhancementReporterProperties
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
@ -92,10 +90,4 @@ public class PolarisCircuitBreakerAutoConfiguration {
return new CircuitBreakerConfigModifier(properties, polarisCircuitBreakerProperties);
}
@Bean
@ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor")
public LoadBalancerInterceptorBeanPostProcessor loadBalancerInterceptorBeanPostProcessor() {
return new LoadBalancerInterceptorBeanPostProcessor();
}
}

@ -1,69 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.circuitbreaker.instrument.resttemplate;
import java.io.IOException;
import java.net.URI;
import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.EnhancedRestTemplateWrapInterceptor;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* PolarisLoadBalancerInterceptor is a wrapper of LoadBalancerInterceptor.
*
* @author Shedfree Wu
*/
public class PolarisLoadBalancerInterceptor extends LoadBalancerInterceptor {
private final LoadBalancerClient loadBalancer;
private final LoadBalancerRequestFactory requestFactory;
private final EnhancedPluginRunner enhancedPluginRunner;
public PolarisLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory,
EnhancedPluginRunner enhancedPluginRunner) {
super(loadBalancer, requestFactory);
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
this.enhancedPluginRunner = enhancedPluginRunner;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String peerServiceName = originalUri.getHost();
Assert.state(peerServiceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
if (enhancedPluginRunner != null) {
EnhancedRestTemplateWrapInterceptor enhancedRestTemplateWrapInterceptor = new EnhancedRestTemplateWrapInterceptor(enhancedPluginRunner, loadBalancer);
return enhancedRestTemplateWrapInterceptor.intercept(request, peerServiceName, this.requestFactory.createRequest(request, body, execution));
}
else {
return super.intercept(request, body, execution);
}
}
}

@ -1,126 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.circuitbreaker.instrument.resttemplate;
import java.io.IOException;
import java.net.URI;
import java.util.Objects;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpResponse;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Test for ${@link PolarisLoadBalancerInterceptor}.
*
* @author Shedfree Wu
*/
@ExtendWith(MockitoExtension.class)
class PolarisLoadBalancerInterceptorTest {
@Mock
private LoadBalancerClient loadBalancer;
@Mock
private LoadBalancerRequestFactory requestFactory;
@Mock
private EnhancedPluginRunner enhancedPluginRunner;
@Mock
private HttpRequest request;
@Mock
private ClientHttpRequestExecution execution;
private PolarisLoadBalancerInterceptor interceptor;
private byte[] body;
@BeforeEach
void setUp() {
body = "test body".getBytes();
}
@Test
void testInterceptWithEnhancedPlugin() throws IOException {
// Arrange
ClientHttpResponse mockResponse = mock(ClientHttpResponse.class);
interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, enhancedPluginRunner);
URI uri = URI.create("http://test-service/path");
when(request.getURI()).thenReturn(uri);
when(loadBalancer.execute(any(), any())).thenReturn(mockResponse);
// Act
ClientHttpResponse response = interceptor.intercept(request, body, execution);
// Assert
Assertions.assertTrue(Objects.equals(mockResponse, response) || response instanceof PolarisCircuitBreakerHttpResponse);
}
@Test
void testInterceptWithoutEnhancedPlugin() throws IOException {
// Arrange
ClientHttpResponse mockResponse = mock(ClientHttpResponse.class);
interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, null);
URI uri = URI.create("http://test-service/path");
when(request.getURI()).thenReturn(uri);
when(loadBalancer.execute(any(), any())).thenReturn(mockResponse);
// Act
ClientHttpResponse response = interceptor.intercept(request, body, execution);
// Assert
Assertions.assertEquals(mockResponse, response);
}
@Test
void testInterceptWithInvalidUri() {
// Arrange
interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, enhancedPluginRunner);
when(request.getURI()).thenReturn(URI.create("http:///path")); // Invalid URI without host
// Act & Assert
Exception exception = Assertions.assertThrows(IllegalStateException.class, () -> {
interceptor.intercept(request, body, execution);
});
Assertions.assertTrue(exception.getMessage().contains("Request URI does not contain a valid hostname"));
}
@Test
void testConstructor() {
// Act
interceptor = new PolarisLoadBalancerInterceptor(loadBalancer, requestFactory, enhancedPluginRunner);
// Assert
Assertions.assertNotNull(interceptor);
}
}

@ -83,9 +83,16 @@ public class PolarisConfigAutoConfiguration {
springValueRegistry, configFileService, contextRefresher, polarisSDKContextManager.getSDKContext());
}
/**
* In some scenarios, configurations are not annotated with @RefreshScope but are refreshed directly by listening to events.
* In such cases, it is necessary to actively execute putRefreshScopePrefixKey.
*/
@Bean
public SpringValueRegistry springValueRegistry() {
return new SpringValueRegistry();
SpringValueRegistry springValueRegistry = new SpringValueRegistry();
// TODO: support dynamic config
springValueRegistry.putRefreshScopePrefixKey("spring.cloud.openfeign.client");
return springValueRegistry;
}
@Bean

@ -19,6 +19,7 @@ package com.tencent.cloud.polaris.eager.instrument.feign;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.net.URI;
import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient;
import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient;
@ -60,18 +61,24 @@ public class FeignEagerLoadSmartLifecycle implements SmartLifecycle {
// if feignClient contains url, it doesn't need to eager load.
if (StringUtils.isEmpty(feignClient.url())) {
// support variables and default values.
String feignName = hardCodedTarget.name();
LOG.info("[{}] eager-load start", feignName);
String url = hardCodedTarget.name();
// refer to FeignClientFactoryBean, convert to URL, then take the host as the service name.
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "http://" + url;
}
String serviceName = URI.create(url).getHost();
LOG.info("[{}] eager-load start, feign name: {}", serviceName, hardCodedTarget.name());
if (polarisDiscoveryClient != null) {
polarisDiscoveryClient.getInstances(feignName);
polarisDiscoveryClient.getInstances(serviceName);
}
else if (polarisReactiveDiscoveryClient != null) {
polarisReactiveDiscoveryClient.getInstances(feignName).subscribe();
polarisReactiveDiscoveryClient.getInstances(serviceName).subscribe();
}
else {
LOG.warn("[{}] no discovery client found.", feignName);
LOG.warn("[{}] no discovery client found.", serviceName);
}
LOG.info("[{}] eager-load end", feignName);
LOG.info("[{}] eager-load end", serviceName);
}
}
}

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>tsf-example</artifactId>
<groupId>com.tencent.cloud</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>consumer-demo-retry</artifactId>
<dependencies>
<dependency>
<groupId>com.tencent.cloud</groupId>
<artifactId>spring-cloud-starter-tencent-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>

@ -0,0 +1,41 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.tsf.demo.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.tsf.annotation.EnableTsf;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableFeignClients // 使用Feign微服务调用时请启用
@EnableTsf
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

@ -0,0 +1,170 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.tsf.demo.consumer.controller;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import com.tencent.cloud.common.util.PolarisCompletableFutureUtils;
import com.tencent.cloud.tsf.demo.consumer.proxy.ProviderDemoService;
import com.tencent.cloud.tsf.demo.consumer.proxy.ProviderService;
import com.tencent.polaris.api.utils.StringUtils;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.tsf.core.TsfContext;
import org.springframework.tsf.core.entity.Tag;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private ProviderService providerService;
@Autowired
private ProviderDemoService providerDemoService;
@RequestMapping(value = "/echo-rest/{str}", method = RequestMethod.GET)
public String restProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue) {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "rest");
Map<String, String> mTags = new HashMap<>();
mTags.put("rest-trace-key1", "value1");
mTags.put("rest-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
try {
return restTemplate.getForObject("http://provider-demo/echo/" + str, String.class);
}
catch (CallAbortedException callAbortedException) {
return callAbortedException.getMessage();
}
}
@RequestMapping(value = "/echo-slow-rest/{str}", method = RequestMethod.GET)
public String restSlowProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue) {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "rest");
Map<String, String> mTags = new HashMap<>();
mTags.put("rest-trace-key1", "value1");
mTags.put("rest-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
try {
return restTemplate.getForObject("http://provider-demo/echo/slow/" + str, String.class);
}
catch (CallAbortedException callAbortedException) {
return callAbortedException.getMessage();
}
}
@RequestMapping(value = "/echo-rest-async/{str}", method = RequestMethod.GET)
public String restAsync(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue) throws ExecutionException, InterruptedException {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "rest");
Map<String, String> mTags = new HashMap<>();
mTags.put("rest-trace-key1", "value1");
mTags.put("rest-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
CompletableFuture<String> echoFuture = PolarisCompletableFutureUtils.supplyAsync(() -> {
try {
return restTemplate.getForObject("http://provider-demo/echo/" + str, String.class);
}
catch (CallAbortedException callAbortedException) {
return callAbortedException.getMessage();
}
});
return echoFuture.get();
}
@RequestMapping(value = "/echo-feign/{str}", method = RequestMethod.GET)
public String feignProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue) {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "feign");
Map<String, String> mTags = new HashMap<>();
mTags.put("feign-trace-key1", "value1");
mTags.put("feign-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
try {
return providerDemoService.echo(str);
}
catch (CallAbortedException callAbortedException) {
return callAbortedException.getMessage();
}
}
@RequestMapping(value = "/echo-slow-feign/{str}", method = RequestMethod.GET)
public String feignSlowProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue,
@RequestParam(required = false) String delay) {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "feign");
Map<String, String> mTags = new HashMap<>();
mTags.put("feign-trace-key1", "value1");
mTags.put("feign-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
int sleepTime = delay == null ? 1000 : Integer.parseInt(delay);
try {
return providerDemoService.echoSlow(str, sleepTime);
}
catch (CallAbortedException callAbortedException) {
return callAbortedException.getMessage();
}
}
@RequestMapping(value = "/echo-feign-url/{str}", method = RequestMethod.GET)
public String feignUrlProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue) {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "feignUrl");
Map<String, String> mTags = new HashMap<>();
mTags.put("feignUrl-trace-key1", "value1");
mTags.put("feignUrl-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
return providerService.echo(str);
}
}

@ -0,0 +1,54 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.tsf.demo.consumer.controller;
import com.tencent.polaris.api.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.tsf.core.TsfContext;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class SdkBaseTest {
private static final Logger LOG = LoggerFactory.getLogger(SdkBaseTest.class);
@Autowired
private RestTemplate restTemplate;
// 调用一次provider接口
@RequestMapping(value = "/echo-once/{str}", method = RequestMethod.GET)
public String restOnceProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue) {
if (!StringUtils.isEmpty(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
LOG.info("start call echo-once");
String result = restTemplate.getForObject("http://provider-demo/echo/" + str, String.class);
LOG.info("end call echo-once, the result is : " + result);
return result;
}
}

@ -0,0 +1,32 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.tsf.demo.consumer.entity;
/**
* Metadata.
*/
public class CustomMetadata {
private String name;
private String value;
public CustomMetadata(String name, String value) {
this.name = name;
this.value = value;
}
}

@ -0,0 +1,36 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.tsf.demo.consumer.proxy;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "${provider.name:provider-demo}")
public interface ProviderDemoService {
@RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
String echo(@PathVariable("str") String str);
@RequestMapping(value = "/echo/error/{str}", method = RequestMethod.GET)
String echoError(@PathVariable("str") String str);
@RequestMapping(value = "/echo/slow/{str}", method = RequestMethod.GET)
String echoSlow(@PathVariable("str") String str, @RequestParam("delay") int delay);
}

@ -0,0 +1,44 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.tsf.demo.consumer.proxy;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* URLFeignClient
* 使provider-ip:provider-port
*/
@FeignClient(name = "provider", url = "http://127.0.0.1:18081", fallback = FeignClientFallback.class)
public interface ProviderService {
@RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
String echo(@PathVariable("str") String str);
}
@Component
class FeignClientFallback implements ProviderService {
@Override
public String echo(String str) {
return "tsf-fault-tolerance-" + str;
}
}

@ -0,0 +1,20 @@
server:
port: 18083
spring:
application:
name: consumer-demo
config:
import: optional:polaris
feign:
tsf:
enabled: true
#本地测试时打开
#tsf_namespace_id: default_namespace
logging:
file:
name: /tsf-demo-logs/${spring.application.name}/root.log
level:
root: INFO

@ -67,6 +67,26 @@ public class ConsumerController {
}
}
@RequestMapping(value = "/echo-slow-rest/{str}", method = RequestMethod.GET)
public String restSlowProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue) {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "rest");
Map<String, String> mTags = new HashMap<>();
mTags.put("rest-trace-key1", "value1");
mTags.put("rest-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
try {
return restTemplate.getForObject("http://provider-demo/echo/slow/" + str, String.class);
}
catch (CallAbortedException callAbortedException) {
return callAbortedException.getMessage();
}
}
@RequestMapping(value = "/echo-rest-async/{str}", method = RequestMethod.GET)
public String restAsync(@PathVariable String str,
@ -111,6 +131,28 @@ public class ConsumerController {
}
}
@RequestMapping(value = "/echo-slow-feign/{str}", method = RequestMethod.GET)
public String feignSlowProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,
@RequestParam(required = false) String tagValue,
@RequestParam(required = false) String delay) {
if (StringUtils.isNotBlank(tagName)) {
TsfContext.putTag(tagName, tagValue);
}
TsfContext.putTag("operation", "feign");
Map<String, String> mTags = new HashMap<>();
mTags.put("feign-trace-key1", "value1");
mTags.put("feign-trace-key2", "value2");
TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE);
int sleepTime = delay == null ? 1000 : Integer.parseInt(delay);
try {
return providerDemoService.echoSlow(str, sleepTime);
}
catch (CallAbortedException callAbortedException) {
return callAbortedException.getMessage();
}
}
@RequestMapping(value = "/echo-feign-url/{str}", method = RequestMethod.GET)
public String feignUrlProvider(@PathVariable String str,
@RequestParam(required = false) String tagName,

@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "${provider.name:provider-demo}")
@FeignClient(name = "${provider.name:http://provider-demo}")
public interface ProviderDemoService {
@RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
String echo(@PathVariable("str") String str);

@ -15,6 +15,7 @@
<modules>
<module>provider-demo</module>
<module>consumer-demo-retry</module>
<module>consumer-demo</module>
</modules>
</project>

@ -29,7 +29,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@ -52,6 +54,8 @@ public class ProviderController {
@Autowired
private ProviderNameConfig providerNameConfig;
private boolean ifBadGateway = false;
// 获取本机ip
public static String getInet4Address() {
Enumeration<NetworkInterface> nis;
@ -85,6 +89,11 @@ public class ProviderController {
@RequestMapping(value = "/echo/{param}", method = RequestMethod.GET)
public ResponseEntity<String> echo(@PathVariable String param) {
if (ifBadGateway) {
LOG.info("Provider Demo is called wrong.");
return new ResponseEntity<>("failed for call provider demo service. Address: " + getInet4Address(), HttpStatus.BAD_GATEWAY);
}
int status;
String responseBody;
@ -149,4 +158,17 @@ public class ProviderController {
LOG.info("provider-demo -- unit response info: [" + result + "]");
return result;
}
@GetMapping("/setBadGateway")
public String setBadGateway(@RequestParam boolean param) {
this.ifBadGateway = param;
if (param) {
LOG.info("info is set to return HttpStatus.BAD_GATEWAY.");
return "info is set to return HttpStatus.BAD_GATEWAY.";
}
else {
LOG.info("info is set to return HttpStatus.OK.");
return "info is set to return HttpStatus.OK.";
}
}
}

@ -0,0 +1,66 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.rpc.enhancement.beanprocessor;
import com.tencent.cloud.rpc.enhancement.instrument.resttemplate.PolarisBlockingLoadBalancerClient;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
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.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
/**
* BlockingLoadBalancerClientBeanPostProcessor is used to wrap the default BlockingLoadBalancerClient implementation and returns a custom PolarisBlockingLoadBalancerClient.
*
* @author Shedfree Wu
*/
public class BlockingLoadBalancerClientBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware, Ordered {
/**
* The order of the bean post processor. If user wants to wrap it(CustomBlockingLoadBalancerClient -> PolarisBlockingLoadBalancerClient), CustomBlockingLoadBalancerClientBeanPostProcessor's order should be bigger than ${@link ORDER}.
*/
public static final int ORDER = 0;
private BeanFactory factory;
@Override
public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
this.factory = beanFactory;
}
@Override
public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
if (bean instanceof BlockingLoadBalancerClient) {
BlockingLoadBalancerClient delegate = (BlockingLoadBalancerClient) bean;
ReactiveLoadBalancer.Factory<ServiceInstance> requestFactory = this.factory.getBean(ReactiveLoadBalancer.Factory.class);
EnhancedPluginRunner pluginRunner = this.factory.getBean(EnhancedPluginRunner.class);
return new PolarisBlockingLoadBalancerClient(requestFactory, delegate, pluginRunner);
}
return bean;
}
@Override
public int getOrder() {
return ORDER;
}
}

@ -23,6 +23,7 @@ import java.util.List;
import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled;
import com.tencent.cloud.polaris.context.PolarisSDKContextManager;
import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration;
import com.tencent.cloud.rpc.enhancement.beanprocessor.BlockingLoadBalancerClientBeanPostProcessor;
import com.tencent.cloud.rpc.enhancement.instrument.feign.EnhancedFeignBeanPostProcessor;
import com.tencent.cloud.rpc.enhancement.instrument.feign.PolarisLoadBalancerFeignRequestTransformer;
import com.tencent.cloud.rpc.enhancement.instrument.filter.EnhancedReactiveFilter;
@ -188,6 +189,12 @@ public class RpcEnhancementAutoConfiguration {
return new PolarisLoadBalancerRequestTransformer();
}
@Bean
@ConditionalOnClass(name = "org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor")
public BlockingLoadBalancerClientBeanPostProcessor loadBalancerInterceptorBeanPostProcessor() {
return new BlockingLoadBalancerClientBeanPostProcessor();
}
}

@ -58,24 +58,24 @@ public class EnhancedRestTemplateWrapInterceptor {
}
public ClientHttpResponse intercept(HttpRequest request, String serviceId,
LoadBalancerRequest<ClientHttpResponse> loadBalancerRequest) throws IOException {
public <T> T intercept(HttpRequest httpRequest, String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> loadBalancerRequest) throws IOException {
EnhancedPluginContext enhancedPluginContext = new EnhancedPluginContext();
URI serviceUrl = request.getURI();
if (request instanceof ServiceRequestWrapper) {
serviceUrl = ((ServiceRequestWrapper) request).getRequest().getURI();
URI serviceUrl = httpRequest.getURI();
if (httpRequest instanceof ServiceRequestWrapper) {
serviceUrl = ((ServiceRequestWrapper) httpRequest).getRequest().getURI();
}
EnhancedRequestContext enhancedRequestContext = EnhancedRequestContext.builder()
.httpHeaders(request.getHeaders())
.httpMethod(request.getMethod())
.url(request.getURI())
.httpHeaders(httpRequest.getHeaders())
.httpMethod(httpRequest.getMethod())
.url(httpRequest.getURI())
.serviceUrl(serviceUrl)
.build();
enhancedPluginContext.setRequest(enhancedRequestContext);
enhancedPluginContext.setOriginRequest(request);
enhancedPluginContext.setOriginRequest(httpRequest);
enhancedPluginContext.setLocalServiceInstance(pluginRunner.getLocalServiceInstance());
@ -85,16 +85,27 @@ public class EnhancedRestTemplateWrapInterceptor {
pluginRunner.run(EnhancedPluginType.Client.PRE, enhancedPluginContext);
startMillis = System.currentTimeMillis();
ClientHttpResponse response = delegate.execute(serviceId, loadBalancerRequest);
T response = null;
// retry rest template, serviceInstance is not null
if (serviceInstance != null) {
response = delegate.execute(serviceId, serviceInstance, loadBalancerRequest);
}
else {
response = delegate.execute(serviceId, loadBalancerRequest);
}
// get target instance after execute
enhancedPluginContext.setTargetServiceInstance((ServiceInstance) MetadataContextHolder.get()
.getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), request.getURI());
.getLoadbalancerMetadata().get(LOAD_BALANCER_SERVICE_INSTANCE), httpRequest.getURI());
enhancedPluginContext.setDelay(System.currentTimeMillis() - startMillis);
EnhancedResponseContext enhancedResponseContext = EnhancedResponseContext.builder()
.httpStatus(response.getRawStatusCode())
.httpHeaders(response.getHeaders())
.build();
EnhancedResponseContext.EnhancedContextResponseBuilder enhancedResponseContextBuilder = EnhancedResponseContext.builder();
if (response instanceof ClientHttpResponse) {
enhancedResponseContextBuilder.httpStatus(((ClientHttpResponse) response).getStatusCode().value());
enhancedResponseContextBuilder.httpHeaders(((ClientHttpResponse) response).getHeaders());
}
EnhancedResponseContext enhancedResponseContext = enhancedResponseContextBuilder.build();
enhancedPluginContext.setResponse(enhancedResponseContext);
// Run post enhanced plugins.
@ -114,7 +125,7 @@ public class EnhancedRestTemplateWrapInterceptor {
if (existFallback) {
Object fallbackResponse = fallbackResponseValue.getObjectValue().orElse(null);
if (fallbackResponse instanceof ClientHttpResponse) {
return (ClientHttpResponse) fallbackResponse;
return (T) fallbackResponse;
}
}
throw callAbortedException;

@ -0,0 +1,74 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.rpc.enhancement.instrument.resttemplate;
import java.io.IOException;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUtils;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import org.springframework.http.HttpRequest;
/**
* PolarisBlockingLoadBalancerClient is a wrapper of BlockingLoadBalancerClient.
*
* @author Shedfree Wu
*/
public class PolarisBlockingLoadBalancerClient extends BlockingLoadBalancerClient {
private BlockingLoadBalancerClient delegate;
private final EnhancedPluginRunner enhancedPluginRunner;
public PolarisBlockingLoadBalancerClient(ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory,
BlockingLoadBalancerClient delegate, EnhancedPluginRunner enhancedPluginRunner) {
super(loadBalancerClientFactory);
this.delegate = delegate;
this.enhancedPluginRunner = enhancedPluginRunner;
}
/**
* common rest template.
*/
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
HttpRequest httpRequest = LoadBalancerUtils.getHttpRequestIfAvailable(request);
if (httpRequest == null || enhancedPluginRunner == null) {
return delegate.execute(serviceId, request);
}
EnhancedRestTemplateWrapInterceptor enhancedRestTemplateWrapInterceptor = new EnhancedRestTemplateWrapInterceptor(enhancedPluginRunner, delegate);
return enhancedRestTemplateWrapInterceptor.intercept(httpRequest, serviceId, null, request);
}
/**
* retry rest template.
*/
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
HttpRequest httpRequest = LoadBalancerUtils.getHttpRequestIfAvailable(LoadBalancerUtils.getDelegateLoadBalancerRequestIfAvailable(request));
if (httpRequest == null || serviceInstance == null || enhancedPluginRunner == null) {
return delegate.execute(serviceId, serviceInstance, request);
}
EnhancedRestTemplateWrapInterceptor enhancedRestTemplateWrapInterceptor = new EnhancedRestTemplateWrapInterceptor(enhancedPluginRunner, delegate);
return enhancedRestTemplateWrapInterceptor.intercept(httpRequest, serviceId, serviceInstance, request);
}
}

@ -0,0 +1,75 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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 org.springframework.cloud.client.loadbalancer;
import java.lang.reflect.Field;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
public final class LoadBalancerUtils {
private static final Logger logger = LoggerFactory.getLogger(LoadBalancerUtils.class);
private LoadBalancerUtils() {
}
/**
* if request is a BlockingLoadBalancerRequest, return its HttpRequest.
*/
public static HttpRequest getHttpRequestIfAvailable(LoadBalancerRequest<?> request) {
if (request instanceof BlockingLoadBalancerRequest) {
BlockingLoadBalancerRequest blockingRequest = (BlockingLoadBalancerRequest) request;
return blockingRequest.getHttpRequest();
}
else {
if (logger.isDebugEnabled()) {
logger.debug("LoadBalancerRequest is not a BlockingLoadBalancerRequest, request:{}", request);
}
return null;
}
}
/**
* if request is a LoadBalancerRequestAdapter(RetryLoadBalancerInterceptor), return its delegate.
*/
public static LoadBalancerRequest<?> getDelegateLoadBalancerRequestIfAvailable(LoadBalancerRequest<?> request) {
if (!(request instanceof LoadBalancerRequestAdapter)) {
if (logger.isDebugEnabled()) {
logger.debug("LoadBalancerRequest is not a LoadBalancerRequestAdapter, request:{}", request);
}
return request;
}
try {
Field delegateField = LoadBalancerRequestAdapter.class.getDeclaredField("delegate");
delegateField.setAccessible(true);
return (LoadBalancerRequest<?>) delegateField.get(request);
}
catch (Exception e) {
// ignore
if (logger.isDebugEnabled()) {
logger.debug("Failed to get delegate from LoadBalancerRequestAdapter, request:{}", request, e);
}
}
return null;
}
}

@ -15,9 +15,8 @@
* specific language governing permissions and limitations under the License.
*/
package com.tencent.cloud.polaris.circuitbreaker.beanprocessor;
package com.tencent.cloud.rpc.enhancement.beanprocessor;
import com.tencent.cloud.polaris.circuitbreaker.instrument.resttemplate.PolarisLoadBalancerInterceptor;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@ -27,19 +26,16 @@ import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test for ${@link LoadBalancerInterceptorBeanPostProcessor}.
* Test for ${@link BlockingLoadBalancerClientBeanPostProcessor}.
*
* @author Shedfree Wu
*/
class LoadBalancerInterceptorBeanPostProcessorTest {
class BlockingLoadBalancerClientBeanPostProcessorTest {
@Mock
private BeanFactory beanFactory;
@ -53,12 +49,12 @@ class LoadBalancerInterceptorBeanPostProcessorTest {
@Mock
private EnhancedPluginRunner pluginRunner;
private LoadBalancerInterceptorBeanPostProcessor processor;
private BlockingLoadBalancerClientBeanPostProcessor processor;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
processor = new LoadBalancerInterceptorBeanPostProcessor();
processor = new BlockingLoadBalancerClientBeanPostProcessor();
processor.setBeanFactory(beanFactory);
// Setup mock behavior
@ -67,22 +63,6 @@ class LoadBalancerInterceptorBeanPostProcessorTest {
when(beanFactory.getBean(EnhancedPluginRunner.class)).thenReturn(pluginRunner);
}
@Test
void testPostProcessBeforeInitializationWithLoadBalancerInterceptor() {
// Arrange
LoadBalancerInterceptor originalInterceptor = mock(LoadBalancerInterceptor.class);
String beanName = "testBean";
// Act
Object result = processor.postProcessBeforeInitialization(originalInterceptor, beanName);
// Assert
Assertions.assertInstanceOf(PolarisLoadBalancerInterceptor.class, result);
verify(beanFactory).getBean(LoadBalancerRequestFactory.class);
verify(beanFactory).getBean(LoadBalancerClient.class);
verify(beanFactory).getBean(EnhancedPluginRunner.class);
}
@Test
void testPostProcessBeforeInitializationWithNonLoadBalancerInterceptor() {
// Arrange
@ -102,26 +82,6 @@ class LoadBalancerInterceptorBeanPostProcessorTest {
int order = processor.getOrder();
// Assert
Assertions.assertEquals(LoadBalancerInterceptorBeanPostProcessor.POLARIS_LOAD_BALANCER_INTERCEPTOR_POST_PROCESSOR_ORDER, order);
}
@Test
void testSetBeanFactory() {
// Arrange
BeanFactory newBeanFactory = mock(BeanFactory.class);
LoadBalancerInterceptorBeanPostProcessor newProcessor = new LoadBalancerInterceptorBeanPostProcessor();
// Act
newProcessor.setBeanFactory(newBeanFactory);
// Assert
// Verify the bean factory is set by trying to process a bean
LoadBalancerInterceptor interceptor = mock(LoadBalancerInterceptor.class);
when(newBeanFactory.getBean(LoadBalancerRequestFactory.class)).thenReturn(requestFactory);
when(newBeanFactory.getBean(LoadBalancerClient.class)).thenReturn(loadBalancerClient);
when(newBeanFactory.getBean(EnhancedPluginRunner.class)).thenReturn(pluginRunner);
Object result = newProcessor.postProcessBeforeInitialization(interceptor, "testBean");
Assertions.assertInstanceOf(PolarisLoadBalancerInterceptor.class, result);
Assertions.assertEquals(BlockingLoadBalancerClientBeanPostProcessor.ORDER, order);
}
}

@ -1,302 +0,0 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.rpc.enhancement.instrument.resttemplate;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import com.tencent.cloud.common.constant.ContextConstant;
import com.tencent.cloud.common.metadata.MetadataContextHolder;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginContext;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginRunner;
import com.tencent.cloud.rpc.enhancement.plugin.EnhancedPluginType;
import com.tencent.polaris.circuitbreak.client.exception.CallAbortedException;
import com.tencent.polaris.metadata.core.MetadataType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.cloud.client.loadbalancer.ServiceRequestWrapper;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test for {@link EnhancedRestTemplateWrapInterceptor}.
*
* @author Shedfree Wu
*/
@ExtendWith(MockitoExtension.class)
class EnhancedRestTemplateWrapInterceptorTest {
@Mock
private EnhancedPluginRunner pluginRunner;
@Mock
private LoadBalancerClient delegate;
@Mock
private HttpRequest request;
@Mock
private ClientHttpResponse response;
@Mock
private ServiceRequestWrapper serviceRequestWrapper;
@Mock
private ServiceInstance localServiceInstance;
private EnhancedRestTemplateWrapInterceptor interceptor;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
interceptor = new EnhancedRestTemplateWrapInterceptor(pluginRunner, delegate);
}
@Test
void testInterceptWithNormalRequest() throws IOException {
// Arrange
URI uri = URI.create("http://test-service/api");
HttpHeaders headers = new HttpHeaders();
headers.add("test-header", "test-value");
when(request.getURI()).thenReturn(uri);
when(request.getHeaders()).thenReturn(headers);
when(request.getMethod()).thenReturn(HttpMethod.GET);
when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance);
when(delegate.execute(any(), any())).thenReturn(response);
when(response.getRawStatusCode()).thenReturn(200);
when(response.getHeaders()).thenReturn(new HttpHeaders());
// Act
interceptor.intercept(request, "test-service", mock(LoadBalancerRequest.class));
// Assert
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class));
// Verify context setup
ArgumentCaptor<EnhancedPluginContext> contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class);
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture());
EnhancedPluginContext capturedContext = contextCaptor.getValue();
Assertions.assertEquals(uri, capturedContext.getRequest().getUrl());
Assertions.assertEquals(uri, capturedContext.getRequest().getServiceUrl());
Assertions.assertEquals(headers, capturedContext.getRequest().getHttpHeaders());
Assertions.assertEquals(HttpMethod.GET, capturedContext.getRequest().getHttpMethod());
Assertions.assertEquals(localServiceInstance, capturedContext.getLocalServiceInstance());
}
@Test
void testInterceptWithServiceRequestWrapper() throws IOException {
// Arrange
URI originalUri = URI.create("http://original-service/api");
URI wrappedUri = URI.create("http://wrapped-service/api");
HttpHeaders headers = new HttpHeaders();
when(serviceRequestWrapper.getURI()).thenReturn(wrappedUri);
when(serviceRequestWrapper.getRequest()).thenReturn(request);
when(serviceRequestWrapper.getHeaders()).thenReturn(headers);
when(serviceRequestWrapper.getMethod()).thenReturn(HttpMethod.POST);
when(request.getURI()).thenReturn(originalUri);
when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance);
when(delegate.execute(any(), any())).thenReturn(response);
when(response.getRawStatusCode()).thenReturn(200);
when(response.getHeaders()).thenReturn(new HttpHeaders());
// Act
interceptor.intercept(serviceRequestWrapper, "test-service", mock(LoadBalancerRequest.class));
// Assert
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class));
ArgumentCaptor<EnhancedPluginContext> contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class);
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture());
EnhancedPluginContext capturedContext = contextCaptor.getValue();
Assertions.assertEquals(wrappedUri, capturedContext.getRequest().getUrl());
Assertions.assertEquals(originalUri, capturedContext.getRequest().getServiceUrl());
Assertions.assertEquals(serviceRequestWrapper, capturedContext.getOriginRequest());
}
@Test
void testInterceptWithFallback() throws IOException {
// Arrange
URI originalUri = URI.create("http://original-service/api");
URI wrappedUri = URI.create("http://wrapped-service/api");
HttpHeaders headers = new HttpHeaders();
CallAbortedException abortedException = new CallAbortedException("test-error", null);
when(serviceRequestWrapper.getURI()).thenReturn(wrappedUri);
when(serviceRequestWrapper.getRequest()).thenReturn(request);
when(serviceRequestWrapper.getHeaders()).thenReturn(headers);
when(serviceRequestWrapper.getMethod()).thenReturn(HttpMethod.POST);
when(request.getURI()).thenReturn(originalUri);
when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance);
doThrow(abortedException)
.when(pluginRunner)
.run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class));
Object fallbackResponse = new MockClientHttpResponse();
MetadataContextHolder.get().getMetadataContainer(MetadataType.APPLICATION, true).
putMetadataObjectValue(ContextConstant.CircuitBreaker.CIRCUIT_BREAKER_FALLBACK_HTTP_RESPONSE, fallbackResponse);
// Act
interceptor.intercept(serviceRequestWrapper, "test-service", mock(LoadBalancerRequest.class));
// Assert
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class));
ArgumentCaptor<EnhancedPluginContext> contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class);
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture());
EnhancedPluginContext capturedContext = contextCaptor.getValue();
Assertions.assertEquals(wrappedUri, capturedContext.getRequest().getUrl());
Assertions.assertEquals(originalUri, capturedContext.getRequest().getServiceUrl());
Assertions.assertEquals(serviceRequestWrapper, capturedContext.getOriginRequest());
}
@Test
void testInterceptWithNoFallback() {
// Arrange
URI originalUri = URI.create("http://original-service/api");
URI wrappedUri = URI.create("http://wrapped-service/api");
HttpHeaders headers = new HttpHeaders();
CallAbortedException abortedException = new CallAbortedException("test-error", null);
when(serviceRequestWrapper.getURI()).thenReturn(wrappedUri);
when(serviceRequestWrapper.getRequest()).thenReturn(request);
when(serviceRequestWrapper.getHeaders()).thenReturn(headers);
when(serviceRequestWrapper.getMethod()).thenReturn(HttpMethod.POST);
when(request.getURI()).thenReturn(originalUri);
when(pluginRunner.getLocalServiceInstance()).thenReturn(localServiceInstance);
doThrow(abortedException)
.when(pluginRunner)
.run(any(), any());
// Act
Assertions.assertThrows(CallAbortedException.class, () -> {
interceptor.intercept(serviceRequestWrapper, "test-service", mock(LoadBalancerRequest.class));
});
}
@Test
void testInterceptWithNullLocalServiceInstance() throws IOException {
// Arrange
URI uri = URI.create("http://test-service/api");
when(request.getURI()).thenReturn(uri);
when(request.getHeaders()).thenReturn(new HttpHeaders());
when(request.getMethod()).thenReturn(HttpMethod.GET);
when(pluginRunner.getLocalServiceInstance()).thenReturn(null);
when(delegate.execute(any(), any())).thenReturn(response);
when(response.getRawStatusCode()).thenReturn(200);
when(response.getHeaders()).thenReturn(new HttpHeaders());
// Act
interceptor.intercept(request, "test-service", mock(LoadBalancerRequest.class));
// Assert
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), any(EnhancedPluginContext.class));
ArgumentCaptor<EnhancedPluginContext> contextCaptor = ArgumentCaptor.forClass(EnhancedPluginContext.class);
verify(pluginRunner).run(eq(EnhancedPluginType.Client.PRE), contextCaptor.capture());
EnhancedPluginContext capturedContext = contextCaptor.getValue();
assertThat(capturedContext.getLocalServiceInstance()).isNull();
}
@Test
void testExceptionHandling() throws IOException {
// Arrange
LoadBalancerRequest<ClientHttpResponse> loadBalancerRequest = mock(LoadBalancerRequest.class);
IOException expectedException = new IOException("Test exception");
when(delegate.execute(anyString(), any(LoadBalancerRequest.class)))
.thenThrow(expectedException);
// Act & Assert
Exception actualException = Assertions.assertThrows(IOException.class, () -> {
interceptor.intercept(request, "test-service", loadBalancerRequest);
});
// Verify exception handling
verify(pluginRunner, times(1))
.run(eq(EnhancedPluginType.Client.EXCEPTION), any(EnhancedPluginContext.class));
// Verify finally block is executed
verify(pluginRunner, times(1))
.run(eq(EnhancedPluginType.Client.FINALLY), any(EnhancedPluginContext.class));
// Verify the thrown exception is the same
Assertions.assertEquals(expectedException, actualException);
}
static class MockClientHttpResponse implements ClientHttpResponse {
@Override
public HttpStatus getStatusCode() throws IOException {
return null;
}
@Override
public int getRawStatusCode() throws IOException {
return 0;
}
@Override
public String getStatusText() throws IOException {
return null;
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return null;
}
@Override
public HttpHeaders getHeaders() {
return null;
}
}
}
Loading…
Cancel
Save