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