diff --git a/CHANGELOG.md b/CHANGELOG.md index bc95b7dd8..51b815950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java deleted file mode 100644 index 72596b238..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessor.java +++ /dev/null @@ -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; - } -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java index 0f01fa1bf..1bb62d804 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/config/PolarisCircuitBreakerAutoConfiguration.java @@ -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(); - } - } diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptor.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptor.java deleted file mode 100644 index fbf077f6f..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/main/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptor.java +++ /dev/null @@ -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); - } - } -} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptorTest.java b/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptorTest.java deleted file mode 100644 index 50f7b9ae6..000000000 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/instrument/resttemplate/PolarisLoadBalancerInterceptorTest.java +++ /dev/null @@ -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); - } -} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java index 2652804cd..b29e2d1cb 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java @@ -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 diff --git a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadSmartLifecycle.java b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadSmartLifecycle.java index da1bbbaca..0f7804cc9 100644 --- a/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadSmartLifecycle.java +++ b/spring-cloud-starter-tencent-polaris-discovery/src/main/java/com/tencent/cloud/polaris/eager/instrument/feign/FeignEagerLoadSmartLifecycle.java @@ -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); } } } diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/pom.xml b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/pom.xml new file mode 100644 index 000000000..b39c426ed --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + tsf-example + com.tencent.cloud + ${revision} + ../pom.xml + + consumer-demo-retry + + + + com.tencent.cloud + spring-cloud-starter-tencent-all + + + + org.springframework.retry + spring-retry + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + \ No newline at end of file diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/ConsumerApplication.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/ConsumerApplication.java new file mode 100644 index 000000000..87694e5b2 --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/ConsumerApplication.java @@ -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(); + } +} diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/ConsumerController.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/ConsumerController.java new file mode 100644 index 000000000..09326b06d --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/ConsumerController.java @@ -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 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 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 mTags = new HashMap<>(); + mTags.put("rest-trace-key1", "value1"); + mTags.put("rest-trace-key2", "value2"); + TsfContext.putTags(mTags, Tag.ControlFlag.TRANSITIVE); + CompletableFuture 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 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 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 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); + } +} diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/SdkBaseTest.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/SdkBaseTest.java new file mode 100644 index 000000000..1b68d6777 --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/SdkBaseTest.java @@ -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; + } +} diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/entity/CustomMetadata.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/entity/CustomMetadata.java new file mode 100644 index 000000000..f8c72a055 --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/entity/CustomMetadata.java @@ -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; + } +} diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderDemoService.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderDemoService.java new file mode 100644 index 000000000..d615ff08a --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderDemoService.java @@ -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); +} diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderService.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderService.java new file mode 100644 index 000000000..abc552faf --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderService.java @@ -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; + +/** + * 测试通过URL配置FeignClient + * 使用时修改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; + } +} diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/resources/application.yml b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/resources/application.yml new file mode 100644 index 000000000..eb7a01b09 --- /dev/null +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo-retry/src/main/resources/application.yml @@ -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 diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/ConsumerController.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/ConsumerController.java index 25e100fe7..09326b06d 100644 --- a/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/ConsumerController.java +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/controller/ConsumerController.java @@ -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 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 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, diff --git a/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderDemoService.java b/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderDemoService.java index d615ff08a..a47ebd428 100644 --- a/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderDemoService.java +++ b/spring-cloud-tencent-examples/tsf-example/consumer-demo/src/main/java/com/tencent/cloud/tsf/demo/consumer/proxy/ProviderDemoService.java @@ -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); diff --git a/spring-cloud-tencent-examples/tsf-example/pom.xml b/spring-cloud-tencent-examples/tsf-example/pom.xml index a6db70129..979efb5f9 100644 --- a/spring-cloud-tencent-examples/tsf-example/pom.xml +++ b/spring-cloud-tencent-examples/tsf-example/pom.xml @@ -15,6 +15,7 @@ provider-demo + consumer-demo-retry consumer-demo diff --git a/spring-cloud-tencent-examples/tsf-example/provider-demo/src/main/java/com/tencent/cloud/tsf/demo/provider/ProviderController.java b/spring-cloud-tencent-examples/tsf-example/provider-demo/src/main/java/com/tencent/cloud/tsf/demo/provider/ProviderController.java index eaba3e34e..40c3f0758 100644 --- a/spring-cloud-tencent-examples/tsf-example/provider-demo/src/main/java/com/tencent/cloud/tsf/demo/provider/ProviderController.java +++ b/spring-cloud-tencent-examples/tsf-example/provider-demo/src/main/java/com/tencent/cloud/tsf/demo/provider/ProviderController.java @@ -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 nis; @@ -85,6 +89,11 @@ public class ProviderController { @RequestMapping(value = "/echo/{param}", method = RequestMethod.GET) public ResponseEntity 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."; + } + } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/beanprocessor/BlockingLoadBalancerClientBeanPostProcessor.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/beanprocessor/BlockingLoadBalancerClientBeanPostProcessor.java new file mode 100644 index 000000000..f48144b22 --- /dev/null +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/beanprocessor/BlockingLoadBalancerClientBeanPostProcessor.java @@ -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 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; + } +} diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java index 96b886a91..4e45577cb 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/config/RpcEnhancementAutoConfiguration.java @@ -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(); + } + } diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java index 2659ce340..cb29597f3 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptor.java @@ -58,24 +58,24 @@ public class EnhancedRestTemplateWrapInterceptor { } - public ClientHttpResponse intercept(HttpRequest request, String serviceId, - LoadBalancerRequest loadBalancerRequest) throws IOException { + public T intercept(HttpRequest httpRequest, String serviceId, ServiceInstance serviceInstance, + LoadBalancerRequest 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; diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/PolarisBlockingLoadBalancerClient.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/PolarisBlockingLoadBalancerClient.java new file mode 100644 index 000000000..aa9a89774 --- /dev/null +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/PolarisBlockingLoadBalancerClient.java @@ -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 loadBalancerClientFactory, + BlockingLoadBalancerClient delegate, EnhancedPluginRunner enhancedPluginRunner) { + super(loadBalancerClientFactory); + this.delegate = delegate; + this.enhancedPluginRunner = enhancedPluginRunner; + } + + /** + * common rest template. + */ + @Override + public T execute(String serviceId, LoadBalancerRequest 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 execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest 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); + } +} diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUtils.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUtils.java new file mode 100644 index 000000000..f9552f798 --- /dev/null +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/org/springframework/cloud/client/loadbalancer/LoadBalancerUtils.java @@ -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; + } + +} diff --git a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessorTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/beanprocessor/BlockingLoadBalancerClientBeanPostProcessorTest.java similarity index 51% rename from spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessorTest.java rename to spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/beanprocessor/BlockingLoadBalancerClientBeanPostProcessorTest.java index 6e5f55d50..0d50adcfd 100644 --- a/spring-cloud-starter-tencent-polaris-circuitbreaker/src/test/java/com/tencent/cloud/polaris/circuitbreaker/beanprocessor/LoadBalancerInterceptorBeanPostProcessorTest.java +++ b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/beanprocessor/BlockingLoadBalancerClientBeanPostProcessorTest.java @@ -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); } } diff --git a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptorTest.java b/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptorTest.java deleted file mode 100644 index fd7d99e08..000000000 --- a/spring-cloud-tencent-rpc-enhancement/src/test/java/com/tencent/cloud/rpc/enhancement/instrument/resttemplate/EnhancedRestTemplateWrapInterceptorTest.java +++ /dev/null @@ -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 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 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 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 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 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; - } - } -}