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.
pull/2342/head
Freeman Lau 3 years ago
parent eb59569d35
commit 2342f23bf3

@ -43,11 +43,19 @@
<optional>true</optional>
</dependency>
<!-- support Feign client configuration start -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<artifactId>spring-cloud-openfeign-core</artifactId>
<version>3.0.4</version> <!-- need greater than 3.0.4, support CircuitBreakerNameResolver -->
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<optional>true</optional>
</dependency>
<!-- support Feign client configuration end -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -80,6 +88,23 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
</exclusion>
<exclusion>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
</dependencies>
</project>

@ -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.
*
* <p> <strong>note:</strong> 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();
}
}

@ -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<List<AbstractCircuitBreakerFactory>> provider) {
List<AbstractCircuitBreakerFactory> 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 {

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

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

@ -97,7 +97,7 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

Loading…
Cancel
Save