上一節我們通過單元測試驗證了線程隔離的正確性,這一節我們來驗證我們斷路器的正確性,主要包括:
- 驗證配置正確加載:即我們在 Spring 配置(例如
application.yml
)中的加入的 Resilience4j 的配置被正確加載應用了。 - 驗證斷路器是基於服務和方法打開的,也就是某個微服務的某個方法斷路器打開但是不會影響這個微服務的其他方法調用
驗證配置正確加載
與之前驗證重試類似,我們可以定義不同的 FeignClient,之后檢查 resilience4j 加載的斷路器配置來驗證線程隔離配置的正確加載。
並且,與重試配置不同的是,通過系列前面的源碼分析,我們知道 spring-cloud-openfeign 的 FeignClient 其實是懶加載的。所以我們實現的斷路器也是懶加載的,需要先調用,之后才會初始化斷路器。所以這里我們需要先進行調用之后,再驗證斷路器配置。
首先定義兩個 FeignClient,微服務分別是 testService1 和 testService2,contextId 分別是 testService1Client 和 testService2Client
@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
@FeignClient(name = "testService2", contextId = "testService2Client")
public interface TestService2Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
然后,我們增加 Spring 配置,並且給兩個微服務都添加一個實例,使用 SpringExtension 編寫單元測試類:
//SpringExtension也包含了 Mockito 相關的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//默認請求重試次數為 3
"resilience4j.retry.configs.default.maxAttempts=3",
// testService2Client 里面的所有方法請求重試次數為 2
"resilience4j.retry.configs.testService2Client.maxAttempts=2",
//默認斷路器配置
"resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=2",
//testService2Client 的 斷路器配置
"resilience4j.circuitbreaker.configs.testService2Client.failureRateThreshold=30",
"resilience4j.circuitbreaker.configs.testService2Client.minimumNumberOfCalls=10",
})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
public static class App {
@Bean
public DiscoveryClient discoveryClient() {
//模擬兩個服務實例
ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
ServiceInstance service2Instance2 = Mockito.spy(ServiceInstance.class);
Map<String, String> zone1 = Map.ofEntries(
Map.entry("zone", "zone1")
);
when(service1Instance1.getMetadata()).thenReturn(zone1);
when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
when(service1Instance1.getHost()).thenReturn("www.httpbin.org");
when(service1Instance1.getPort()).thenReturn(80);
when(service2Instance2.getInstanceId()).thenReturn("service1Instance2");
when(service2Instance2.getHost()).thenReturn("httpbin.org");
when(service2Instance2.getPort()).thenReturn(80);
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
Mockito.when(spy.getInstances("testService1"))
.thenReturn(List.of(service1Instance1));
Mockito.when(spy.getInstances("testService2"))
.thenReturn(List.of(service2Instance2));
return spy;
}
}
}
編寫測試代碼,驗證配置正確:
@Test
public void testConfigureCircuitBreaker() {
//防止斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
//調用下這兩個 FeignClient 確保對應的 NamedContext 被初始化
testService1Client.anything();
testService2Client.anything();
//驗證斷路器的實際配置,符合我們的填入的配置
List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
Set<String> collect = circuitBreakers.stream().map(CircuitBreaker::getName)
.filter(name -> {
try {
return name.contains(TestService1Client.class.getMethod("anything").toGenericString())
|| name.contains(TestService2Client.class.getMethod("anything").toGenericString());
} catch (NoSuchMethodException e) {
return false;
}
}).collect(Collectors.toSet());
Assertions.assertEquals(collect.size(), 2);
circuitBreakers.forEach(circuitBreaker -> {
if (circuitBreaker.getName().contains(TestService1Client.class.getName())) {
Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) DEFAULT_FAILURE_RATE_THRESHOLD);
Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), DEFAULT_MINIMUM_NUMBER_OF_CALLS);
} else if (circuitBreaker.getName().contains(TestService2Client.class.getName())) {
Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) TEST_SERVICE_2_FAILURE_RATE_THRESHOLD);
Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), TEST_SERVICE_2_MINIMUM_NUMBER_OF_CALLS);
}
});
}
驗證斷路器是基於服務和方法打開的。
我們給 TestService1Client 添加一個方法:
@GetMapping("/status/500")
String testCircuitBreakerStatus500();
這個方法一定會調用失敗,從而導致斷路器打開。經過 2 次失敗以上后(因為配置最少觸發斷路器打開的請求個數為 2),驗證斷路器狀態:
@Test
public void testCircuitBreakerOpenBasedOnServiceAndMethod() {
//防止斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
AtomicBoolean passed = new AtomicBoolean(false);
for (int i = 0; i < 10; i++) {
//多次調用會導致斷路器打開
try {
System.out.println(testService1Client.testCircuitBreakerStatus500());
} catch(Exception e) {}
List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
circuitBreakers.stream().filter(circuitBreaker -> {
return circuitBreaker.getName().contains("testCircuitBreakerStatus500")
&& circuitBreaker.getName().contains("TestService1Client");
}).findFirst().ifPresent(circuitBreaker -> {
//驗證對應微服務和方法的斷路器被打開
if (circuitBreaker.getState().equals(CircuitBreaker.State.OPEN)) {
passed.set(true);
//斷路器打開后,調用其他方法,不會拋出斷路器打開異常
testService1Client.testAnything();
}
});
}
Assertions.assertTrue(passed.get());
}
這樣,我們就成功驗證了,驗證斷路器是基於服務和方法打開的。
微信搜索“我的編程喵”關注公眾號,每日一刷,輕松提升技術,斬獲各種offer: