You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}

核心问题分析

  1. AOP代理失效
    测试类中通过new IntegrationServiceImpl(webClient,endpoint)手动实例化服务类,而Resilience4J的@CircuitBreaker注解依赖Spring AOP生成代理对象来实现断路器逻辑。手动实例化的对象没有经过Spring容器的代理增强,断路器完全未生效,所有请求都不会被统计到断路器的调用记录中。

  2. WireMock URL匹配不匹配
    测试中WireMock配置的URL是/api/order/EEE03740511,但实际请求的caseNumber是requestDTO.getNumber()即"1232",导致WireMock的stub无法匹配请求,实际返回的可能不是500错误,断路器无法统计到失败请求。

  3. 异常未明确配置为失败触发条件
    默认情况下Resilience4J会将所有RuntimeException视为失败,但为了确保自定义异常RecoverableException和AppException被正确统计,需要明确配置断路器的记录异常类型。

修复方案

  1. 通过Spring容器获取代理后的服务实例
    修改测试类,移除手动实例化代码,直接通过@Autowired注入服务:
@Autowired
IntegrationService integrationService;

@Before
public void setUp() {
    WIREMOCK_SERVER.resetAll();
    requestDTO = RequestDTO.builder()
            .number("1232")
            .message("Hello")
            .build();
}
  1. 修正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"))
);
  1. 明确配置断路器的失败异常
    在application-test.properties中添加:
resilience4j.circuitbreaker.instances.CircuitBreaker.recordExceptions=com.example.demo.Exception.RecoverableException,com.example.demo.Exception.AppException

验证步骤

  1. 应用上述修复后,重新运行测试用例。
  2. 检查断路器统计:可以在测试中添加日志打印断路器的调用统计,确认失败请求数是否达到阈值:
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("CircuitBreaker");
log.info("断路器统计:{}", circuitBreaker.getMetrics());
assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState());

内容的提问来源于stack exchange,提问作者Lasal Hettiarachchi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 20:04:53