Mastering OpenFeign: Elegant Service Calls and Load Balancing in Spring Cloud
This article compares RestTemplate and OpenFeign for inter‑service calls, demonstrates how to set up OpenFeign with Spring Cloud, covers advanced configurations such as timeouts, retries, interceptors, logging, fallbacks, and circuit breaking, explains load‑balancing strategies, shares a complete microservice implementation, and resolves common pitfalls.
1. Service‑call approaches
Using RestTemplate with @LoadBalanced requires manual URL concatenation, verbose parameter handling, repeated error handling, and provides no interface contract.
URL concatenation is cumbersome
Complex parameters lead to verbose code
Error handling is duplicated
No interface contract
OpenFeign offers a declarative client that makes remote calls appear as local method invocations.
Declarative and concise
Interface serves as documentation
Unified exception handling
Supports complex parameters
2. OpenFeign quick start
2.1 Add dependencies
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>2.2 Enable Feign
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.laok.client")
public class ArticleServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ArticleServiceApplication.class, args);
}
}2.3 Define Feign client
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);
}
@Service
public class ArticleService {
@Autowired
private UserClient userClient;
public User getUser(Long userId) {
return userClient.getUser(userId);
}
}3. Advanced Feign configuration
3.1 Timeout settings
# application.yml
feign:
client:
config:
default:
connectTimeout: 5000 # ms
readTimeout: 10000 # ms
user-service:
connectTimeout: 3000
readTimeout: 5000 @Configuration
public class FeignConfig {
@Bean
public Request.Options options() {
return new Request.Options(5000, 10000);
}
}3.2 Retry mechanism
@Configuration
public class FeignRetryConfig {
@Bean
public Retryer feignRetryer() {
// max 3 attempts, initial interval 100ms, multiplier 1.5
return new Retryer.Default(100, 1000, 3);
}
} # application.yml
feign:
client:
config:
default:
retryer: com.netflix.client.config.DefaultClientConfigImpl3.3 Request/response interceptor
@Component
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
@Autowired
private RequestContextHolder requestContextHolder;
@Override
public void apply(RequestTemplate template) {
// add common headers
template.header("X-Source", "blog-micro");
template.header("X-Version", "1.0");
// propagate Authorization token
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
String token = attrs.getRequest().getHeader("Authorization");
if (StringUtils.hasText(token)) {
template.header("Authorization", token);
}
}
log.info("Feign Request: {} {}", template.method(), template.url());
}
}3.4 Logging levels
# application.yml
logging:
level:
com.laok.client: DEBUG
feign:
client:
config:
default:
loggerLevel: fullNONE – no logs
BASIC – method, URL, status, execution time
HEADERS – BASIC plus request/response headers
FULL – HEADERS plus request/response bodies
3.5 Fallback (circuit breaker)
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public Result<UserVO> getUser(Long id) {
log.error("调用用户服务失败: {}", cause.getMessage());
return Result.error(500, "用户服务不可用,请稍后重试");
}
};
}
} @FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient { /* ... */ } # application.yml
feign:
circuitbreaker:
enabled: true4. Load balancing details
4.1 Default strategy
Spring Cloud 2021.x uses Spring Cloud LoadBalancer (replaces Ribbon) with a random strategy when configured.
spring:
cloud:
loadbalancer:
ribbon:
enabled: false
configurations: random4.2 Custom strategies
@Configuration
public class LoadBalancerConfig {
// Random strategy
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment env, LoadBalancerClientFactory factory) {
String name = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
// Round‑Robin (default)
@Bean
public ReactorLoadBalancer<ServiceInstance> roundRobinLoadBalancer(Environment env, LoadBalancerClientFactory factory) {
String name = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}4.3 Weight‑based balancing
# Nacos instance weight
spring:
cloud:
nacos:
discovery:
weight: 80 # default 100, higher weight gets more traffic4.4 Same‑cluster preference
spring:
cloud:
nacos:
discovery:
cluster-name: BJ # Beijing cluster
prefer-same-cluster: true5. Full blog system implementation
5.1 Shared API module
blog-micro/
├── common-api/ # shared interfaces
│ ├── pom.xml
│ └── src/main/java/com/laok/client/
│ ├── UserClient.java
│ ├── ArticleClient.java
│ ├── CommentClient.java
│ └── fallback/...
├── user-service/
├── article-service/
└── comment-service/5.2 Define shared interfaces
@FeignClient(name = "user-service", path = "/api/user", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/{id}")
Result<UserVO> getUser(@PathVariable("id") Long id);
@PostMapping("/batch")
Result<List<UserVO>> batchGetUsers(@RequestBody List<Long> ids);
@PutMapping("/{id}/increase-view")
Result<Void> increaseViewCount(@PathVariable("id") Long id);
}5.3 Server implementation
@RestController
@RequestMapping("/api/user")
public class UserController implements UserClient {
@Autowired
private UserService userService;
@Override
public Result<UserVO> getUser(Long id) {
return Result.success(userService.getById(id));
}
@Override
public Result<List<UserVO>> batchGetUsers(List<Long> ids) {
return Result.success(userService.batchGet(ids));
}
@Override
public Result<Void> increaseViewCount(Long id) {
userService.increaseViewCount(id);
return Result.success();
}
}5.4 Client usage with parallel calls
@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
private UserClient userClient;
@Autowired
private CommentClient commentClient;
@Override
public ArticleDetailVO getDetail(Long articleId) {
Article article = articleMapper.selectById(articleId);
CompletableFuture<UserVO> userFuture = CompletableFuture.supplyAsync(() ->
userClient.getUser(article.getUserId()).getData());
CompletableFuture<List<CommentVO>> commentsFuture = CompletableFuture.supplyAsync(() ->
commentClient.listByArticle(articleId));
UserVO author = userFuture.join();
List<CommentVO> comments = commentsFuture.join();
return ArticleDetailVO.builder()
.article(article)
.author(author)
.comments(comments)
.build();
}
}6. Performance optimizations
6.1 Connection pool
# application.yml
feign:
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
time-to-live: 9006.2 Compression
# application.yml
feign:
compression:
request:
enabled: true
mime-types: text/xml,application/json
min-request-size: 2048
response:
enabled: true6.3 Batch call optimization
// Bad: looped single calls
public List<ArticleVO> getArticles(List<Long> ids) {
return ids.stream()
.map(id -> articleClient.getArticle(id))
.collect(Collectors.toList());
}
// Good: single batch endpoint
public List<ArticleVO> getArticles(List<Long> ids) {
return articleClient.batchGetArticles(ids); // one call
}7. Common issues and solutions
7.1 Token lost in Feign call
Symptom: 401 Unauthorized because Feign does not forward headers.
Solution: Add an interceptor that copies the Authorization header.
@Component
public class TokenInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
String token = attrs.getRequest().getHeader("Authorization");
if (StringUtils.hasText(token)) {
template.header("Authorization", token);
}
}
}
}7.2 Null parameter causes 400
Symptom: feign.FeignException$BadRequest: [400] when a GET parameter is null.
Solution: Mark the parameter as optional or provide a default value.
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/search")
Result<List<UserVO>> search(@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "page", defaultValue = "1") Integer page);
}7.3 Service call timeout
Symptom: feign.RetryableException: Read timed out.
Solution: Adjust connectTimeout and readTimeout per business needs.
# application.yml
feign:
client:
config:
default:
connectTimeout: 3000
readTimeout: 30000 # give complex queries enough timeSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Coder Trainee
Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
