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
parent
eb59569d35
commit
2342f23bf3
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue