Resilience4J Circuit Breaker未按预期工作,请求问题排查协助
Resilience4J Circuit Breaker 未触发OPEN状态问题排查
问题背景
使用resilience4j-spring-boot3:2.1.0实现断路器功能,编写测试用例模拟100次500错误请求,但断言断路器进入OPEN状态始终失败,断路器未按预期触发。
测试用例代码
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = DemoApplication.class) @TestPropertySource(locations = "classpath:application-test.properties") public class IntegrationServiceImplTest extends WireMockTestUtil { IntegrationService integrationService; @Autowired CircuitBreakerRegistry circuitBreakerRegistry; @Autowired WebClient webClient; @Value("${api.endpoint}") private String endpoint; private RequestDTO requestDTO; @Before public void setUp() { integrationService = new IntegrationServiceImpl(webClient,endpoint); WIREMOCK_SERVER.resetAll(); requestDTO = RequestDTO.builder() .number("1232") .message("Hello") .build(); } @Test public void testCRMAPICircuitBreaker_whenFailureThresholdExceeded(){ WIREMOCK_SERVER.stubFor( WireMock .patch(WireMock .urlPathEqualTo( endpoint.replace("{casenumber}", "EEE03740511") ) ) .willReturn( WireMock.aResponse() .withStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()) .withHeader("Content-Type", "application/json")) ); for (int i = 0; i < 100; i++) { try { integrationService.callService(requestDTO); } catch (Exception ignored) { } } CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("CircuitBreaker"); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState()); } }
配置文件
api.endpoint=/api/order/{casenumber} resilience4j.circuitbreaker.instances.CircuitBreaker.registerHealthIndicator=true resilience4j.circuitbreaker.instances.CircuitBreaker.slidingWindowSize=10 resilience4j.circuitbreaker.instances.CircuitBreaker.minimumNumberOfCalls=8 resilience4j.circuitbreaker.instances.CircuitBreaker.failureRateThreshold=50 resilience4j.circuitbreaker.instances.CircuitBreaker.waitDurationInOpenState=30s resilience4j.circuitbreaker.instances.CircuitBreaker.permittedNumberOfCallsInHalfOpenState=3 resilience4j.circuitbreaker.instances.CircuitBreaker.automaticTransitionFromOpenToHalfOpenEnabled=true resilience4j.circuitbreaker.instances.CircuitBreaker.slidingWindowType=COUNT_BASED
服务实现代码
package com.example.demo.service.Impl; import com.example.demo.Exception.AppException; import com.example.demo.Exception.RecoverableException; import com.example.demo.mapper.APIRequestMapper; import com.example.demo.model.APIRequest; import com.example.demo.model.APIResponse; import com.example.demo.model.RequestDTO; import com.example.demo.service.IntegrationService; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.mapstruct.factory.Mappers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientException; import org.springframework.web.reactive.function.client.WebClientResponseException; @Service @Slf4j @AllArgsConstructor @NoArgsConstructor public class IntegrationServiceImpl implements IntegrationService { @Autowired WebClient webClient; @Value("${api.endpoint}") private String endpoint; private final String CircuitBreakerServiceName = "CircuitBreaker"; @Override public void callService(RequestDTO requestDTO) { APIRequest apiRequest = APIRequestMapper.MAPPER.mapToAPIRequest(requestDTO); callAPI(apiRequest,requestDTO.getNumber()); } @CircuitBreaker(name= CircuitBreakerServiceName, fallbackMethod="fallback") public APIResponse callAPI (APIRequest apiRequest,String caseNumber){ APIResponse apiResponse = null; try { apiResponse = webClient .patch() .uri( uriBuilder -> uriBuilder .path(endpoint) .build(caseNumber) ) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .bodyValue(apiRequest) .retrieve() .bodyToMono(APIResponse.class) .block(); } catch (WebClientResponseException ex) { if (ex.getStatusCode().is4xxClientError()) { if (ex.getStatusCode().value() == 401) { log.error("ERROR IN CRM API: WebClient received 401 error :{}.", ex.getResponseBodyAsString()); throw new RecoverableException("Error in server", ex); } else { log.error("ERROR IN CRM API: WebClient received 4xx error :{}.", ex.getResponseBodyAsString()); throw new AppException("500", "Error in application", ex); } } else if (ex.getStatusCode().is5xxServerError()) { log.error("ERROR IN CRM API: WebClient received error :{}.", ex.getResponseBodyAsString()); throw new RecoverableException("Error in server", ex); } else { log.error("ERROR IN CRM API: WebClient received error :{}.", ex.getResponseBodyAsString()); throw new AppException("500", "Error in application", ex); } } catch (WebClientException ex) { log.error("ERROR IN CRM API :", ex); throw new RecoverableException("Error in server", ex); } catch (Exception ex) { log.error("Unexpected Exception In CRM API : ", ex); throw new AppException("500", "Error in application", ex); } return apiResponse; } public APIResponse fallback(APIRequest crmapiRequest, String caseNumber, Exception ex){ throw new RecoverableException("Error in server", ex); } }
核心问题分析
AOP代理失效
测试类中通过new IntegrationServiceImpl(webClient,endpoint)手动实例化服务类,而Resilience4J的@CircuitBreaker注解依赖Spring AOP生成代理对象来实现断路器逻辑。手动实例化的对象没有经过Spring容器的代理增强,断路器完全未生效,所有请求都不会被统计到断路器的调用记录中。WireMock URL匹配不匹配
测试中WireMock配置的URL是/api/order/EEE03740511,但实际请求的caseNumber是requestDTO.getNumber()即"1232",导致WireMock的stub无法匹配请求,实际返回的可能不是500错误,断路器无法统计到失败请求。异常未明确配置为失败触发条件
默认情况下Resilience4J会将所有RuntimeException视为失败,但为了确保自定义异常RecoverableException和AppException被正确统计,需要明确配置断路器的记录异常类型。
修复方案
- 通过Spring容器获取代理后的服务实例
修改测试类,移除手动实例化代码,直接通过@Autowired注入服务:
@Autowired IntegrationService integrationService; @Before public void setUp() { WIREMOCK_SERVER.resetAll(); requestDTO = RequestDTO.builder() .number("1232") .message("Hello") .build(); }
- 修正WireMock URL匹配逻辑
两种可选方案:
- 让测试用例的
requestDTO的number与stub中的一致:
requestDTO = RequestDTO.builder() .number("EEE03740511") .message("Hello") .build();
- 或者使用模糊匹配URL,适配任意caseNumber:
WIREMOCK_SERVER.stubFor( WireMock .patch(WireMock.urlPathMatching("/api/order/.*")) .willReturn( WireMock.aResponse() .withStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()) .withHeader("Content-Type", "application/json")) );
- 明确配置断路器的失败异常
在application-test.properties中添加:
resilience4j.circuitbreaker.instances.CircuitBreaker.recordExceptions=com.example.demo.Exception.RecoverableException,com.example.demo.Exception.AppException
验证步骤
- 应用上述修复后,重新运行测试用例。
- 检查断路器统计:可以在测试中添加日志打印断路器的调用统计,确认失败请求数是否达到阈值:
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("CircuitBreaker"); log.info("断路器统计:{}", circuitBreaker.getMetrics()); assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState());
内容的提问来源于stack exchange,提问作者Lasal Hettiarachchi
相关产品推荐
相关产品推荐

