服务调用 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);
}
@FeignClient 的 value 或 name 属性指定服务名,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、响应状态码、执行时间 |
| HEADERS | BASIC + 请求头和响应头 |
| FULL | HEADERS + 请求体和响应体 |
配置日志级别:
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 接口以 Client 或 FeignClient 结尾,如 UserClient、OrderFeignClient。
统一返回值封装
服务端统一返回值格式,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 实现熔断降级。使用连接池提升性能。合理配置超时、监控调用、区分异常类型。