From 2342f23bf3a298291380e7ce1954a2fd60d7c0b9 Mon Sep 17 00:00:00 2001 From: Freeman Lau Date: Fri, 7 Jan 2022 15:08:33 +0800 Subject: [PATCH] feature: support configuration per Feign client Note: it depends on spring cloud openfeign version in classpath. If using spring-cloud-openfeign-core version >= 3.0.4, you can configure for per Feign client. Otherwise, you can only configure for per Feign client' single method. Fix test. --- .../pom.xml | 27 +- .../feign/FeignClientCircuitNameResolver.java | 51 ++++ .../SentinelFeignClientAutoConfiguration.java | 23 ++ ...ientCircuitBreakerRuleIntegrationTest.java | 171 ------------- ...ientCircuitBreakerRuleIntegrationTest.java | 240 ++++++++++++++++++ .../pom.xml | 2 +- 6 files changed, 341 insertions(+), 173 deletions(-) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitNameResolver.java delete mode 100644 spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/CustomFeignClientCircuitBreakerRuleIntegrationTest.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitBreakerRuleIntegrationTest.java diff --git a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/pom.xml index 88e9d15d9..7978dd8fd 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/pom.xml +++ b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/pom.xml @@ -43,11 +43,19 @@ true + org.springframework.cloud - spring-cloud-starter-openfeign + spring-cloud-openfeign-core + 3.0.4 + true + + + io.github.openfeign + feign-core true + org.springframework.boot @@ -80,6 +88,23 @@ test + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.cloud + spring-cloud-openfeign-core + + + io.github.openfeign + feign-core + + + test + + + diff --git a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitNameResolver.java b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitNameResolver.java new file mode 100644 index 000000000..208e2f05f --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitNameResolver.java @@ -0,0 +1,51 @@ +package com.alibaba.cloud.circuitbreaker.sentinel.feign; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + +import feign.Feign; +import feign.Target; + +import org.springframework.cloud.client.circuitbreaker.AbstractCircuitBreakerFactory; +import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; + +/** + * Feign client circuit breaker name resolver. + * + *

note: spring cloud openfeign version need greater than 3.0.4. + * + * @author freeman + * @see CircuitBreakerNameResolver + */ +public class FeignClientCircuitNameResolver implements CircuitBreakerNameResolver { + + private Map configurations; + + public FeignClientCircuitNameResolver(AbstractCircuitBreakerFactory factory) { + configurations = getConfigurations(factory); + } + + @Override + public String resolveCircuitBreakerName(String feignClientName, + Target target, Method method) { + String key = Feign.configKey(target.type(), method); + + if (configurations != null && configurations.containsKey(key)) { + return key; + } + + return feignClientName; + } + + private Map getConfigurations(AbstractCircuitBreakerFactory factory) { + try { + Method getConfigurations = AbstractCircuitBreakerFactory.class.getDeclaredMethod("getConfigurations"); + getConfigurations.setAccessible(true); + return (Map) getConfigurations.invoke(factory); + } catch (Exception ignored) { + } + return Collections.emptyMap(); + } + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/SentinelFeignClientAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/SentinelFeignClientAutoConfiguration.java index 7bdfeb1dd..c2f6c08c2 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/SentinelFeignClientAutoConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/main/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/SentinelFeignClientAutoConfiguration.java @@ -17,6 +17,7 @@ package com.alibaba.cloud.circuitbreaker.sentinel.feign; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -27,11 +28,14 @@ import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; import feign.Feign; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.circuitbreaker.AbstractCircuitBreakerFactory; import org.springframework.cloud.client.circuitbreaker.Customizer; +import org.springframework.cloud.openfeign.CircuitBreakerNameResolver; import org.springframework.cloud.openfeign.FeignClientFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -48,6 +52,25 @@ import org.springframework.context.annotation.Configuration; @EnableConfigurationProperties(SentinelFeignClientProperties.class) public class SentinelFeignClientAutoConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(CircuitBreakerNameResolver.class) + public static class CircuitBreakerNameResolverConfiguration { + + @Bean + @ConditionalOnMissingBean(CircuitBreakerNameResolver.class) + public CircuitBreakerNameResolver feignClientCircuitNameResolver( + ObjectProvider> provider) { + List factories = provider + .getIfAvailable(Collections::emptyList); + if (factories.size() >= 1) { + return new FeignClientCircuitNameResolver(factories.get(0)); + } + throw new IllegalArgumentException( + "need one CircuitBreakerFactory/ReactiveCircuitBreakerFactory, but 0 found."); + } + + } + @Configuration(proxyBeanMethods = false) public static class SentinelCustomizerConfiguration { diff --git a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/CustomFeignClientCircuitBreakerRuleIntegrationTest.java b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/CustomFeignClientCircuitBreakerRuleIntegrationTest.java deleted file mode 100644 index e0fb6088d..000000000 --- a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/CustomFeignClientCircuitBreakerRuleIntegrationTest.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2013-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * 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.alibaba.cloud.circuitbreaker.sentinel.feign; - -import java.util.ArrayList; - -import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.openfeign.EnableFeignClients; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT; - -/** - * @author freeman - */ -@RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = DEFINED_PORT, classes = CustomFeignClientCircuitBreakerRuleIntegrationTest.Application.class, properties = { - "server.port=10101", "feign.circuitbreaker.enabled=true", - "spring.cloud.discovery.client.health-indicator.enabled=false", - "feign.sentinel.default-rule=default", - "feign.sentinel.rules.default[0].grade=2", - "feign.sentinel.rules.default[0].count=1", - "feign.sentinel.rules.default[0].timeWindow=1", - "feign.sentinel.rules.default[0].statIntervalMs=1000", - "feign.sentinel.rules.default[0].minRequestAmount=5", - "feign.sentinel.rules.[UserClient#success(boolean)][0].grade=2", - "feign.sentinel.rules.[UserClient#success(boolean)][0].count=1", - "feign.sentinel.rules.[UserClient#success(boolean)][0].timeWindow=1", - "feign.sentinel.rules.[UserClient#success(boolean)][0].statIntervalMs=1000", - "feign.sentinel.rules.[UserClient#success(boolean)][0].minRequestAmount=5" }) -@DirtiesContext -public class CustomFeignClientCircuitBreakerRuleIntegrationTest { - - @Autowired - private Application.UserClient userClient; - - @Test - public void testConfigSpecificRule() throws Exception { - // test specific configuration is working - - // ok - assertThat(userClient.success(true)).isEqualTo("ok"); - - // occur exception, circuit breaker open - assertThat(userClient.success(false)).isEqualTo("fallback"); - assertThat(userClient.success(false)).isEqualTo("fallback"); - assertThat(userClient.success(false)).isEqualTo("fallback"); - assertThat(userClient.success(false)).isEqualTo("fallback"); - assertThat(userClient.success(false)).isEqualTo("fallback"); - - // test circuit breaker open - assertThat(userClient.success(true)).isEqualTo("fallback"); - assertThat(userClient.success(true)).isEqualTo("fallback"); - - Thread.sleep(1100L); - - // test circuit breaker close - assertThat(userClient.success(true)).isEqualTo("ok"); - } - - @Test - public void testConfigDefaultRule() throws Exception { - // test default configuration is working - - // ok - assertThat(userClient.defaultConfig(true)).isEqualTo("ok"); - - // occur exception, circuit breaker open - assertThat(userClient.defaultConfig(false)).isEqualTo("fallback"); - assertThat(userClient.defaultConfig(false)).isEqualTo("fallback"); - assertThat(userClient.defaultConfig(false)).isEqualTo("fallback"); - assertThat(userClient.defaultConfig(false)).isEqualTo("fallback"); - assertThat(userClient.defaultConfig(false)).isEqualTo("fallback"); - - // test circuit breaker open - assertThat(userClient.defaultConfig(true)).isEqualTo("fallback"); - assertThat(userClient.defaultConfig(true)).isEqualTo("fallback"); - - Thread.sleep(1100L); - - // test circuit breaker close - assertThat(userClient.defaultConfig(true)).isEqualTo("ok"); - } - - @Before - public void reset() { - DegradeRuleManager.loadRules(new ArrayList<>()); - } - - @Configuration - @EnableAutoConfiguration - @RestController - @EnableFeignClients - protected static class Application { - - @FeignClient(value = "user", url = "http://localhost:${server.port}", fallback = UserClientFallback.class) - interface UserClient { - - @GetMapping("/{success}") - String success(@PathVariable boolean success); - - @GetMapping("/default/{success}") - String defaultConfig(@PathVariable boolean success); - } - - @Component - static class UserClientFallback implements UserClient { - - @Override - public String success(boolean success) { - return "fallback"; - } - - @Override - public String defaultConfig(boolean success) { - return "fallback"; - } - } - - @RestController - static class UserController { - - @GetMapping("/{success}") - public String success(@PathVariable boolean success) { - if (success) { - return "ok"; - } - throw new RuntimeException("failed"); - } - - @GetMapping("/default/{success}") - String defaultConfig(@PathVariable boolean success) { - if (success) { - return "ok"; - } - throw new RuntimeException("failed"); - } - } - - } - -} diff --git a/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitBreakerRuleIntegrationTest.java b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitBreakerRuleIntegrationTest.java new file mode 100644 index 000000000..e7271b5be --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-circuitbreaker-sentinel/src/test/java/com/alibaba/cloud/circuitbreaker/sentinel/feign/FeignClientCircuitBreakerRuleIntegrationTest.java @@ -0,0 +1,240 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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.alibaba.cloud.circuitbreaker.sentinel.feign; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import static com.alibaba.cloud.circuitbreaker.sentinel.feign.FeignClientCircuitBreakerRuleIntegrationTest.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT; + +/** + * @author freeman + */ +@SpringBootTest(webEnvironment = DEFINED_PORT, classes = Application.class, properties = { + "server.port=10101", + "feign.circuitbreaker.enabled=true", + "feign.sentinel.default-rule=default", + "feign.sentinel.rules.default[0].grade=2", + "feign.sentinel.rules.default[0].count=2", + "feign.sentinel.rules.default[0].timeWindow=1", + "feign.sentinel.rules.default[0].statIntervalMs=1000", + "feign.sentinel.rules.default[0].minRequestAmount=5", + "feign.sentinel.rules.user[0].grade=2", + "feign.sentinel.rules.user[0].count=2", + "feign.sentinel.rules.user[0].timeWindow=1", + "feign.sentinel.rules.user[0].statIntervalMs=1000", + "feign.sentinel.rules.user[0].minRequestAmount=5", + "feign.sentinel.rules.[UserClient#specificFeignMethod(boolean)][0].grade=2", + "feign.sentinel.rules.[UserClient#specificFeignMethod(boolean)][0].count=1", + "feign.sentinel.rules.[UserClient#specificFeignMethod(boolean)][0].timeWindow=1", + "feign.sentinel.rules.[UserClient#specificFeignMethod(boolean)][0].statIntervalMs=1000", + "feign.sentinel.rules.[UserClient#specificFeignMethod(boolean)][0].minRequestAmount=5" }) +public class FeignClientCircuitBreakerRuleIntegrationTest { + + @Autowired + private Application.UserClient userClient; + @Autowired + private Application.OrderClient orderClient; + + @Test + public void testDefaultRule() throws Exception { + // test default configuration is working + + // ok + assertThat(orderClient.defaultConfig(true)).isEqualTo("ok"); + assertThat(orderClient.defaultConfig(true)).isEqualTo("ok"); + assertThat(orderClient.defaultConfig(true)).isEqualTo("ok"); + + // occur exception + assertThat(orderClient.defaultConfig(false)).isEqualTo("fallback"); + assertThat(orderClient.defaultConfig(false)).isEqualTo("fallback"); + + // test circuit breaker close + assertThat(orderClient.defaultConfig(true)).isEqualTo("ok"); + + // the 3rd exception, circuit breaker open + assertThat(orderClient.defaultConfig(false)).isEqualTo("fallback"); + + // test circuit breaker open + assertThat(orderClient.defaultConfig(true)).isEqualTo("fallback"); + assertThat(orderClient.defaultConfig(true)).isEqualTo("fallback"); + + // longer than timeWindow, circuit breaker half open + Thread.sleep(1100L); + + // let circuit breaker close + assertThat(orderClient.defaultConfig(true)).isEqualTo("ok"); + assertThat(orderClient.defaultConfig(true)).isEqualTo("ok"); + } + + @Test + public void testSpecificFeignRule() throws Exception { + // test specific Feign client configuration is working + + // ok + assertThat(userClient.specificFeign(true)).isEqualTo("ok"); + assertThat(userClient.specificFeign(true)).isEqualTo("ok"); + assertThat(userClient.specificFeign(true)).isEqualTo("ok"); + + // occur exception + assertThat(userClient.specificFeign(false)).isEqualTo("fallback"); + assertThat(userClient.specificFeign(false)).isEqualTo("fallback"); + + // test circuit breaker close + assertThat(userClient.specificFeign(true)).isEqualTo("ok"); + + // the 3rd exception, circuit breaker open + assertThat(userClient.specificFeign(false)).isEqualTo("fallback"); + + // test circuit breaker open + assertThat(userClient.specificFeign(true)).isEqualTo("fallback"); + assertThat(userClient.specificFeign(true)).isEqualTo("fallback"); + + // longer than timeWindow, circuit breaker half open + Thread.sleep(1100L); + + // let circuit breaker close + assertThat(userClient.specificFeign(true)).isEqualTo("ok"); + assertThat(userClient.specificFeign(true)).isEqualTo("ok"); + } + + @Test + public void testSpecificFeignMethodRule() throws Exception { + // test specific Feign client method configuration is working + + // ok + assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok"); + assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok"); + assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok"); + assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok"); + + // occur exception + assertThat(userClient.specificFeignMethod(false)).isEqualTo("fallback"); + + // 1 time exception, circuit breaker is closed(configuration is 1, but we need 2 + // to make it closed) + assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok"); + + // occur the 2nd exception, circuit breaker closed + assertThat(userClient.specificFeignMethod(false)).isEqualTo("fallback"); + + // test circuit breaker is closed + assertThat(userClient.specificFeignMethod(true)).isEqualTo("fallback"); + assertThat(userClient.specificFeignMethod(true)).isEqualTo("fallback"); + + // longer than timeWindow, circuit breaker half open + Thread.sleep(1100L); + + // let circuit breaker close + assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok"); + assertThat(userClient.specificFeignMethod(true)).isEqualTo("ok"); + } + + @Configuration + @EnableAutoConfiguration + @RestController + @EnableFeignClients + protected static class Application { + + @FeignClient(value = "user", url = "http://localhost:${server.port}", fallback = UserClientFallback.class) + interface UserClient { + + @GetMapping("/specificFeign/{success}") + String specificFeign(@PathVariable boolean success); + + @GetMapping("/specificFeignMethod/{success}") + String specificFeignMethod(@PathVariable boolean success); + + } + + @FeignClient(value = "order", url = "http://localhost:${server.port}", fallback = OrderClientFallback.class) + interface OrderClient { + + @GetMapping("/defaultConfig/{success}") + String defaultConfig(@PathVariable boolean success); + + } + + @Component + static class UserClientFallback implements UserClient { + + @Override + public String specificFeign(boolean success) { + return "fallback"; + } + + @Override + public String specificFeignMethod(boolean success) { + return "fallback"; + } + + } + + @Component + static class OrderClientFallback implements OrderClient { + + @Override + public String defaultConfig(boolean success) { + return "fallback"; + } + + } + + @RestController + static class TestController { + + @GetMapping("/specificFeign/{success}") + public String specificFeign(@PathVariable boolean success) { + if (success) { + return "ok"; + } + throw new RuntimeException("failed"); + } + + @GetMapping("/defaultConfig/{success}") + String defaultConfig(@PathVariable boolean success) { + if (success) { + return "ok"; + } + throw new RuntimeException("failed"); + } + + @GetMapping("/specificFeignMethod/{success}") + String specificFeignMethod(@PathVariable boolean success) { + if (success) { + return "ok"; + } + throw new RuntimeException("failed"); + } + + } + + } + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-seata/pom.xml b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-seata/pom.xml index 315856bd2..31fc373b7 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-seata/pom.xml +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-seata/pom.xml @@ -97,7 +97,7 @@ spring-boot-starter-test test - +