跳到主要内容

服务调用 OpenFeign

在微服务架构中,服务之间的调用是核心问题。OpenFeign 是 Spring Cloud 提供的声明式 HTTP 客户端,让服务调用变得简单优雅。

为什么需要声明式客户端

理解 OpenFeign 的价值,需要先看看传统的服务调用方式。

RestTemplate 的局限

Spring 提供的 RestTemplate 是常用的 HTTP 客户端。在微服务中调用其他服务时,需要拼接 URL、处理参数、解析响应:

@Service
public class OrderService {

@Autowired
private RestTemplate restTemplate;

public User getUser(Long userId) {
String url = "http://user-service/user/" + userId;
ResponseEntity<User> response = restTemplate.getForEntity(url, User.class);
return response.getBody();
}

public User createUser(User user) {
String url = "http://user-service/user";
return restTemplate.postForObject(url, user, User.class);
}

public List<User> searchUsers(String name, Integer age) {
String url = String.format(
"http://user-service/user/search?name=%s&age=%d", name, age);
User[] users = restTemplate.getForObject(url, User[].class);
return Arrays.asList(users);
}
}

这种方式的缺点很明显:URL 需要手动拼接,容易出错;参数传递和响应解析需要手动处理;接口定义分散在代码中,不直观;难以维护和测试。

声明式客户端的优势

OpenFeign 采用声明式的方式定义 HTTP 接口。只需要创建一个接口并添加注解,就能完成服务调用:

@FeignClient("user-service")
public interface UserClient {

@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);

@PostMapping("/user")
User createUser(@RequestBody User user);

@GetMapping("/user/search")
List<User> searchUsers(@RequestParam("name") String name,
@RequestParam("age") Integer age);
}

调用时只需要注入接口:

@Service
public class OrderService {

@Autowired
private UserClient userClient;

public User getUser(Long userId) {
return userClient.getUser(userId);
}
}

声明式客户端的优势:接口定义即文档,一目了然;代码简洁,易于维护;支持 Spring MVC 注解,学习成本低;与 LoadBalancer、Sentinel 等组件无缝集成。

快速开始

添加依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

OpenFeign 需要配合 LoadBalancer 使用:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

启用 Feign

在启动类上添加 @EnableFeignClients 注解:

@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}

如果 Feign Client 定义在其他包中,需要指定扫描路径:

@EnableFeignClients(basePackages = "com.example.clients")
// 或者指定具体的类
@EnableFeignClients(clients = {UserClient.class, OrderClient.class})

定义 Feign Client

创建一个接口,使用 @FeignClient 注解声明要调用的服务:

@FeignClient("user-service")
public interface UserClient {

@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);

@GetMapping("/user")
List<User> getAllUsers();

@PostMapping("/user")
User createUser(@RequestBody User user);

@PutMapping("/user/{id}")
User updateUser(@PathVariable("id") Long id, @RequestBody User user);

@DeleteMapping("/user/{id}")
void deleteUser(@PathVariable("id") Long id);
}

@FeignClientvaluename 属性指定服务名,Feign 会从注册中心获取该服务的实例列表并负载均衡。

使用 Feign Client

在需要调用服务的地方注入 Feign Client:

@Service
public class OrderService {

@Autowired
private UserClient userClient;

public Order createOrder(CreateOrderRequest request) {
// 调用用户服务获取用户信息
User user = userClient.getUser(request.getUserId());
if (user == null) {
throw new BusinessException("用户不存在");
}

// 创建订单
Order order = new Order();
order.setUserId(user.getId());
order.setUserName(user.getName());

return order;
}
}

参数传递

OpenFeign 支持 Spring MVC 的参数注解,可以灵活传递各种类型的参数。

路径参数

使用 @PathVariable 传递路径参数:

@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);

@GetMapping("/user/{userId}/order/{orderId}")
Order getOrder(@PathVariable("userId") Long userId,
@PathVariable("orderId") Long orderId);

查询参数

使用 @RequestParam 传递查询参数:

// 单个参数
@GetMapping("/user/search")
User findByName(@RequestParam("name") String name);

// 多个参数
@GetMapping("/user/search")
List<User> search(@RequestParam("name") String name,
@RequestParam("age") Integer age);

// 可选参数
@GetMapping("/user/search")
List<User> search(@RequestParam("name") String name,
@RequestParam(value = "age", required = false) Integer age);

// 多值参数
@GetMapping("/user/search")
List<User> findByIds(@RequestParam("ids") List<Long> ids);

// 使用 Map 传递多个参数
@GetMapping("/user/search")
List<User> search(@RequestParam Map<String, String> params);

请求体

使用 @RequestBody 传递 JSON 请求体:

@PostMapping("/user")
User createUser(@RequestBody User user);

@PutMapping("/user/{id}")
User updateUser(@PathVariable("id") Long id, @RequestBody User user);

Feign 使用 Jackson 序列化请求体,确保类有无参构造函数和必要的 getter/setter。

表单数据

使用 @RequestParam@ModelAttribute 传递表单数据:

// 使用 @RequestParam
@PostMapping("/user/login")
User login(@RequestParam("username") String username,
@RequestParam("password") String password);

// 使用 @ModelAttribute
@PostMapping("/user/register")
User register(@ModelAttribute UserRegisterForm form);

请求头

使用 @RequestHeader 传递请求头:

// 单个请求头
@GetMapping("/user/info")
User getUserInfo(@RequestHeader("Authorization") String token);

// 多个请求头
@GetMapping("/user/info")
User getUserInfo(@RequestHeader("Authorization") String token,
@RequestHeader("X-Request-Id") String requestId);

// 使用 Map 传递多个请求头
@GetMapping("/user/info")
User getUserInfo(@RequestHeader Map<String, String> headers);

Feign Client 配置

全局配置

在 application.yml 中配置 Feign 的全局行为:

feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时(毫秒)
readTimeout: 5000 # 读取超时(毫秒)
loggerLevel: BASIC # 日志级别

针对特定服务配置

可以为不同的服务配置不同的参数:

feign:
client:
config:
user-service:
connectTimeout: 3000
readTimeout: 3000
loggerLevel: FULL
order-service:
connectTimeout: 10000
readTimeout: 30000
loggerLevel: BASIC

使用配置类

通过 Java 配置类自定义 Feign Client:

public class UserClientConfig {

@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}

@Bean
public RequestInterceptor requestInterceptor() {
return template -> {
template.header("X-Client-Type", "feign");
template.header("X-Request-Time", LocalDateTime.now().toString());
};
}

@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}

@FeignClient 中引用配置:

@FeignClient(name = "user-service", configuration = UserClientConfig.class)
public interface UserClient {
// ...
}

注意配置类不要添加 @Configuration 注解,否则会影响所有 Feign Client。

日志配置

Feign 支持四种日志级别:

级别说明
NONE不记录日志(默认)
BASIC记录请求方法、URL、响应状态码、执行时间
HEADERSBASIC + 请求头和响应头
FULLHEADERS + 请求体和响应体

配置日志级别:

feign:
client:
config:
user-service:
loggerLevel: FULL

还需要为 Feign Client 配置 Logger:

logging:
level:
com.example.client.UserClient: DEBUG

Feign 只在 DEBUG 级别才会输出日志。

请求拦截器

请求拦截器可以在请求发送前统一处理,常用于添加认证信息、签名等。

添加认证信息

@Component
public class AuthRequestInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate template) {
// 从上下文获取 Token
String token = SecurityContextHolder.getContext()
.getAuthentication()
.getCredentials()
.toString();

// 添加到请求头
template.header("Authorization", "Bearer " + token);
}
}

这个拦截器会对所有 Feign Client 生效。如果只想对特定 Client 生效,放在对应的配置类中。

添加追踪信息

@Component
public class TraceRequestInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate template) {
// 传递链路追踪信息
String traceId = MDC.get("traceId");
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
}
}

错误处理

自定义错误解码器

Feign 默认将非 2xx 响应解码为 FeignException。可以自定义错误解码器:

public class CustomErrorDecoder implements ErrorDecoder {

@Override
public Exception decode(String methodKey, Response response) {
int status = response.status();

try {
String body = Util.toString(response.body().asReader());

if (status >= 400 && status <= 499) {
return new ClientException(body, status);
}
if (status >= 500) {
return new ServerException(body, status);
}
} catch (IOException e) {
return new DecodeException("Error decoding response", e);
}

return new Default().decode(methodKey, response);
}
}

注册错误解码器:

@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}

使用 try-catch 处理

@Service
public class OrderService {

@Autowired
private UserClient userClient;

public User getUser(Long userId) {
try {
return userClient.getUser(userId);
} catch (FeignException.NotFound e) {
log.warn("用户不存在: {}", userId);
return null;
} catch (FeignException e) {
log.error("调用用户服务失败: {}", e.getMessage());
throw new ServiceException("服务调用失败");
}
}
}

熔断降级

OpenFeign 可以集成 Sentinel 实现熔断降级。

添加依赖

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

开启熔断

feign:
sentinel:
enabled: true

定义降级处理

方式一:实现 Feign Client 接口

@Component
public class UserClientFallback implements UserClient {

@Override
public User getUser(Long id) {
User user = new User();
user.setId(id);
user.setName("降级用户");
return user;
}

@Override
public List<User> getAllUsers() {
return Collections.emptyList();
}

// 其他方法...
}

@FeignClient 中指定降级类:

@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
// ...
}

方式二:使用 FallbackFactory 获取异常信息

@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public User getUser(Long id) {
log.error("调用用户服务失败: {}", cause.getMessage());

User user = new User();
user.setId(id);
user.setName("降级用户");
return user;
}

// 其他方法...
};
}
}
@FeignClient(name = "user-service", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
// ...
}

FallbackFactory 的优势是可以获取到异常信息,根据不同的异常类型返回不同的降级结果。

性能优化

使用连接池

Feign 默认使用 JDK 的 HttpURLConnection,不支持连接池。建议使用 OkHttp 或 Apache HttpClient。

使用 OkHttp:

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
feign:
okhttp:
enabled: true

使用 Apache HttpClient:

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
feign:
httpclient:
enabled: true
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 每个路由的最大连接数
connection-timeout: 5000 # 连接超时

启用 GZIP 压缩

对于传输大量数据的请求,启用 GZIP 压缩:

feign:
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048
response:
enabled: true

合理设置超时

根据服务的响应时间设置合理的超时:

feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
slow-service:
connectTimeout: 10000
readTimeout: 60000

连接超时建议设置较短(1-5秒),读取超时根据服务响应时间设置。

高级特性

继承支持

定义公共接口,Feign Client 和服务端 Controller 都可以实现它:

// 公共接口
public interface UserService {
@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);

@PostMapping("/user")
User createUser(@RequestBody User user);
}

Feign Client 继承:

@FeignClient("user-service")
public interface UserClient extends UserService {
// 可以添加额外的方法
}

服务端实现:

@RestController
public class UserController implements UserService {

@Override
public User getUser(Long id) {
return userService.getById(id);
}

@Override
public User createUser(User user) {
return userService.create(user);
}
}

这种方式可以确保客户端和服务端的接口定义一致,但会增加耦合。

手动创建 Feign Client

除了声明式定义,也可以手动构建 Feign Client:

UserService userService = Feign.builder()
.client(new OkHttpClient())
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(UserService.class, "http://user-service");

使用 URL 属性

可以通过配置动态指定服务地址:

@FeignClient(name = "user-service", url = "${user-service.url}")
public interface UserClient {
// ...
}
user-service:
url: http://localhost:8081

这种方式适用于服务未注册到注册中心的场景。

最佳实践

接口命名规范

Feign Client 接口以 ClientFeignClient 结尾,如 UserClientOrderFeignClient

统一返回值封装

服务端统一返回值格式,Feign Client 处理统一:

@Data
public class Result<T> {
private int code;
private String message;
private T data;
}

@FeignClient("user-service")
public interface UserClient {

@GetMapping("/user/{id}")
Result<User> getUser(@PathVariable("id") Long id);
}

区分业务异常和系统异常

自定义错误解码器,区分业务异常(如用户不存在)和系统异常(如服务不可用)。

配置合理的超时和重试

连接超时不宜过长,读取超时根据接口特性设置。对于非幂等接口,谨慎配置重试。

监控 Feign 调用

通过 Micrometer 监控 Feign 调用:

management:
endpoints:
web:
exposure:
include: metrics

Feign 会自动注册指标,包括调用次数、响应时间、错误率等。

小结

本章详细介绍了 OpenFeign:

OpenFeign 是声明式的 HTTP 客户端,让服务调用更简洁。支持 Spring MVC 注解,参数传递灵活。可以配置超时、日志、拦截器等。集成 Sentinel 实现熔断降级。使用连接池提升性能。合理配置超时、监控调用、区分异常类型。