feature: support feign client custom configuration

There's a potential problem, if CircuitBreakerFactory was injected in spring's lifecycle, will possibly get the raw one(not customized). So, remove ReactiveSentinelCircuitBreakerAutoConfiguration and SentinelCircuitBreakerAutoConfiguration inner configuration class.
pull/2342/head
Freeman Lau
parent 973372194a
commit eb59569d35

@ -43,6 +43,18 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>

@ -19,8 +19,6 @@ package com.alibaba.cloud.circuitbreaker.sentinel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@ -32,6 +30,7 @@ import org.springframework.context.annotation.Configuration;
/**
* @author Eric Zhao
* @author freeman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(
@ -40,28 +39,15 @@ import org.springframework.context.annotation.Configuration;
havingValue = "true", matchIfMissing = true)
public class ReactiveSentinelCircuitBreakerAutoConfiguration {
@Autowired(required = false)
private List<Customizer<ReactiveSentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Bean
@ConditionalOnMissingBean(ReactiveCircuitBreakerFactory.class)
public ReactiveCircuitBreakerFactory reactiveSentinelCircuitBreakerFactory() {
return new ReactiveSentinelCircuitBreakerFactory();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(
name = { "reactor.core.publisher.Mono", "reactor.core.publisher.Flux" })
public static class ReactiveSentinelCustomizerConfiguration {
@Autowired(required = false)
private List<Customizer<ReactiveSentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Autowired(required = false)
private ReactiveSentinelCircuitBreakerFactory factory;
@PostConstruct
public void init() {
customizers.forEach(customizer -> customizer.customize(factory));
}
ReactiveSentinelCircuitBreakerFactory factory = new ReactiveSentinelCircuitBreakerFactory();
customizers.forEach(customizer -> customizer.customize(factory));
return factory;
}
}

@ -19,7 +19,6 @@ package com.alibaba.cloud.circuitbreaker.sentinel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import com.alibaba.csp.sentinel.SphU;
@ -36,6 +35,7 @@ import org.springframework.context.annotation.Configuration;
* Auto configuration for {@link SentinelCircuitBreaker}.
*
* @author Eric Zhao
* @author freeman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SphU.class })
@ -43,26 +43,15 @@ import org.springframework.context.annotation.Configuration;
havingValue = "true", matchIfMissing = true)
public class SentinelCircuitBreakerAutoConfiguration {
@Autowired(required = false)
private List<Customizer<SentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Bean
@ConditionalOnMissingBean(CircuitBreakerFactory.class)
public CircuitBreakerFactory sentinelCircuitBreakerFactory() {
return new SentinelCircuitBreakerFactory();
}
@Configuration(proxyBeanMethods = false)
public static class SentinelCustomizerConfiguration {
@Autowired(required = false)
private List<Customizer<SentinelCircuitBreakerFactory>> customizers = new ArrayList<>();
@Autowired(required = false)
private SentinelCircuitBreakerFactory factory;
@PostConstruct
public void init() {
customizers.forEach(customizer -> customizer.customize(factory));
}
SentinelCircuitBreakerFactory factory = new SentinelCircuitBreakerFactory();
customizers.forEach(customizer -> customizer.customize(factory));
return factory;
}
}

@ -0,0 +1,102 @@
/*
* 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 java.util.List;
import java.util.Objects;
import com.alibaba.cloud.circuitbreaker.sentinel.ReactiveSentinelCircuitBreakerFactory;
import com.alibaba.cloud.circuitbreaker.sentinel.SentinelCircuitBreakerFactory;
import com.alibaba.cloud.circuitbreaker.sentinel.SentinelConfigBuilder;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import feign.Feign;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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.FeignClientFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Auto configuration for feign client circuit breaker rules.
*
* @author freeman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Feign.class, FeignClientFactoryBean.class })
@ConditionalOnProperty(name = "spring.cloud.circuitbreaker.sentinel.enabled",
havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(SentinelFeignClientProperties.class)
public class SentinelFeignClientAutoConfiguration {
@Configuration(proxyBeanMethods = false)
public static class SentinelCustomizerConfiguration {
@Bean
public Customizer<SentinelCircuitBreakerFactory> configureRulesCustomizer(
SentinelFeignClientProperties properties) {
return factory -> {
configureDefault(properties, factory);
configureCustom(properties, factory);
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = { "reactor.core.publisher.Mono",
"reactor.core.publisher.Flux" })
public static class ReactiveSentinelCustomizerConfiguration {
@Bean
public Customizer<ReactiveSentinelCircuitBreakerFactory> reactiveConfigureRulesCustomizer(
SentinelFeignClientProperties properties) {
return factory -> {
configureDefault(properties, factory);
configureCustom(properties, factory);
};
}
}
private static void configureCustom(SentinelFeignClientProperties properties,
AbstractCircuitBreakerFactory factory) {
properties.getRules().forEach((resourceName, degradeRules) -> {
if (!Objects.equals(properties.getDefaultRule(), resourceName)) {
factory.configure(builder -> ((SentinelConfigBuilder) builder)
.rules(properties.getRules().getOrDefault(resourceName,
new ArrayList<>())),
resourceName);
}
});
}
private static void configureDefault(SentinelFeignClientProperties properties,
AbstractCircuitBreakerFactory factory) {
List<DegradeRule> defaultConfigurations = properties.getRules()
.getOrDefault(properties.getDefaultRule(), new ArrayList<>());
factory.configureDefault(
resourceName -> new SentinelConfigBuilder(resourceName.toString())
.entryType(EntryType.OUT).rules(defaultConfigurations).build());
}
}

@ -0,0 +1,42 @@
package com.alibaba.cloud.circuitbreaker.sentinel.feign;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Sentinel feign client properties.
*
* @author freeman
*/
@ConfigurationProperties("feign.sentinel")
public class SentinelFeignClientProperties {
/**
* default rule name
*/
private String defaultRule = "default";
private Map<String, List<DegradeRule>> rules = new HashMap<>();
public String getDefaultRule() {
return defaultRule;
}
public void setDefaultRule(String defaultRule) {
this.defaultRule = defaultRule;
}
public Map<String, List<DegradeRule>> getRules() {
return rules;
}
public void setRules(Map<String, List<DegradeRule>> rules) {
this.rules = rules;
}
}

@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.circuitbreaker.sentinel.SentinelCircuitBreakerAutoConfiguration,\
com.alibaba.cloud.circuitbreaker.sentinel.ReactiveSentinelCircuitBreakerAutoConfiguration
com.alibaba.cloud.circuitbreaker.sentinel.ReactiveSentinelCircuitBreakerAutoConfiguration,\
com.alibaba.cloud.circuitbreaker.sentinel.feign.SentinelFeignClientAutoConfiguration

@ -0,0 +1,171 @@
/*
* 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");
}
}
}
}
Loading…
Cancel
Save