Spring Boot应用中使用Resilience4j时Fallback方法未被调用的问题
我有一个基于微服务的简单应用,包含3个微服务:Movie-Catalog-Service、Ratings-Data-Service、Movie-Info-Service。其中Movie-Catalog-Service依赖Ratings-Data-Service和Movie-Info-Service。我在UserRatingInfo服务类的getUserRating()方法上添加了CircuitBreaker注解,但当我故意不启动Ratings-Data-Service并调用/catalog/10接口时,出现的是:
java.lang.IllegalStateException: No instances available for ratings-data-service
而不是执行fallback方法。
以下是我的相关代码:
1. Eureka Server启动类
package com.example.discoveryserver2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class DiscoveryServer2Application { public static void main(String[] args) { SpringApplication.run(DiscoveryServer2Application.class, args); } }
2. Movie-Catalog-Service启动类
package com.example.moviecatalogservice2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class MovieCatalogService2Application { public static void main(String[] args) { SpringApplication.run(MovieCatalogService2Application.class, args); } @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
3. Movie-Info-Service启动类
package com.example.movieinfoservice2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class MovieInfoService2Application { public static void main(String[] args) { SpringApplication.run(MovieInfoService2Application.class, args); } @Bean public RestTemplate getRestTemplate() { HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); clientHttpRequestFactory.setConnectTimeout(3000); return new RestTemplate(clientHttpRequestFactory); } }
4. Ratings-Data-Service启动类
package com.example.ratingsdataservice2; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RatingsDataService2Application { public static void main(String[] args) { SpringApplication.run(RatingsDataService2Application.class, args); } }
5. Movie-Catalog-Service的CatalogResource类
package com.example.moviecatalogservice2.resources; import java.util.List; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.example.moviecatalogservice2.models.CatalogItem; import com.example.moviecatalogservice2.models.UserRating; import com.example.moviecatalogservice2.services.MovieInfo; import com.example.moviecatalogservice2.services.UserRatingInfo; @RestController @RequestMapping("/catalog") public class CatalogResource { @Autowired private MovieInfo movieInfo; @Autowired private UserRatingInfo userRatingInfo; @RequestMapping("/{userId}") public List<CatalogItem> getCatalog(@PathVariable("userId") String userId) { UserRating userRating = userRatingInfo.getUserRating(userId); return userRating.getRatings().stream() .map(rating -> { return movieInfo.getCatalogItem(rating); }) .collect(Collectors.toList()); } }
6. UserRatingInfo服务类(带CircuitBreaker注解)
package com.example.moviecatalogservice2.services; import java.util.Arrays; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import com.example.moviecatalogservice2.models.Rating; import com.example.moviecatalogservice2.models.UserRating; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; @Service public class UserRatingInfo { @Autowired private RestTemplate restTemplate; @CircuitBreaker(name = "ratings-data-service-cb", fallbackMethod = "getFallbackUserRating") public UserRating getUserRating(String userId) { UserRating userRating = restTemplate.getForObject("http://ratings-data-service/ratingsdata/user/{userId}", UserRating.class, userId); return userRating; } public UserRating getFallbackUserRating(String userId, Exception exc) { UserRating userRating = new UserRating(); userRating.setUserId(userId); userRating.setRatings(Arrays.asList(new Rating("100", 0))); return userRating; } }
7. Movie-Catalog-Service的pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>movie-catalog-service-2</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>movie-catalog-service-2</name> <description>Demo spring project for practicing Resilience4j</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>21</java.version> <spring-cloud.version>2024.0.1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </path> </annotationProcessorPaths> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
问题分析及解决方案
核心原因
你遇到的问题是Resilience4j的断路器没有正确捕获负载均衡器抛出的IllegalStateException,或者因为默认配置的限制导致fallback未被触发。以下是针对性的解决方案:
1. 显式指定CircuitBreaker要捕获的异常类型
修改@CircuitBreaker注解,明确指定要包含的异常类型,确保负载均衡抛出的异常被正确捕获:
@CircuitBreaker( name = "ratings-data-service-cb", fallbackMethod = "getFallbackUserRating", include = {IllegalStateException.class, Exception.class} ) public UserRating getUserRating(String userId) { UserRating userRating = restTemplate.getForObject("http://ratings-data-service/ratingsdata/user/{userId}", UserRating.class, userId); return userRating; }
2. 调整Resilience4j断路器的默认配置
Resilience4j默认的minimumNumberOfCalls为10,意味着需要10次失败才会触发断路器状态变化。你可以在application.yml中配置自定义的断路器参数,降低最小调用次数并明确记录异常:
resilience4j: circuitbreaker: instances: ratings-data-service-cb: minimum-number-of-calls: 1 # 第一次失败就统计 failure-rate-threshold: 50 # 失败率超过50%打开断路器 wait-duration-in-open-state: 10s # 断路器打开后10秒进入半开状态 record-exceptions: - java.lang.IllegalStateException - java.lang.Exception
3. 调整Fallback方法的异常参数类型
将fallback方法的异常参数从Exception改为Throwable,确保能捕获所有类型的异常:
public UserRating getFallbackUserRating(String userId, Throwable exc) { // 打印异常信息方便排查 exc.printStackTrace(); UserRating userRating = new UserRating(); userRating.setUserId(userId); userRating.setRatings(Arrays.asList(new Rating("100", 0))); return userRating; }
4. 确认服务注册状态
访问Eureka Server的控制台(默认地址http://localhost:8761),确认Movie-Catalog-Service已成功注册。虽然Spring Cloud 2024版本会自动注册,但如果存在网络或配置问题,可能导致服务未注册,进而影响负载均衡和断路器的正常工作。
备注:内容来源于stack exchange,提问作者Shri

