Feign 声明式 HTTP 客户端
导学
小崔刚写完一段调用库存服务的代码,白歌 review 时皱了皱眉:
// 小崔写的 RestTemplate 调用代码
String url = "http://FEIXIANG-INVENTORY-SERVICE/inventory/" + productId;
Map<String, Object> result = restTemplate.getForObject(url, Map.class);
"这段代码有什么问题?"白歌问。
小崔想了想:"URL 拼接容易出错,返回 Map 没有类型安全,而且如果我要传 JSON 请求体,还得写一堆 HttpEntity 构造代码..."
"这就是 Feign 要解决的问题——把 HTTP 调用写得像本地方法调用一样。"
定位与问题场景
在微服务中,消费者调用提供者本质上是一次 HTTP 请求。但直接使用 RestTemplate 的痛点:
| 痛点 | RestTemplate 方案 | Feign 方案 |
|---|---|---|
| URL 拼接 | 手动字符串拼接 | 注解声明 |
| 参数绑定 | 手动 URI 变量 | @PathVariable / @RequestParam |
| 请求体 | HttpEntity 包装 | @RequestBody |
| 返回类型 | Map / String 无类型安全 | POJO 自动反序列化 |
| 代码量 | 5-10 行/接口 | 1 个接口定义 |
Feign 的核心设计理念:声明式 HTTP 客户端——只需要定义一个接口并加注解,Feign 在运行时动态生成实现类。
操作前后对比:
// 引入 Feign 前:RestTemplate
public Map checkStock(Long productId) {
String url = "http://FEIXIANG-INVENTORY-SERVICE/inventory/" + productId;
return restTemplate.getForObject(url, Map.class);
}
// 引入 Feign 后:声明式接口
@FeignClient("FEIXIANG-INVENTORY-SERVICE")
public interface InventoryClient {
@GetMapping("/inventory/{productId}")
StockDTO getStock(@PathVariable Long productId);
}
Feign 核心原理
动态代理机制
核心组件
| 组件 | 职责 |
|---|---|
| Contract | 解析 @RequestMapping / @FeignClient 等注解,生成方法元数据 |
| Encoder | 将方法参数编码为 HTTP 请求体(默认 Jackson) |
| Decoder | 将 HTTP 响应体解码为返回类型(默认 Jackson) |
| Client | 真正发起 HTTP 请求的组件(默认 JDK HttpURLConnection) |
| Logger | Feign 日志,记录请求/响应详情 |
| Interceptor | 请求拦截器,可添加统一请求头 |
完整示例:飞翔科技订单服务通过 Feign 调用
示例一:基础调用(查询库存)
库存服务提供者:
@RestController
public class InventoryController {
@GetMapping("/inventory/{productId}")
public StockDTO getStock(@PathVariable Long productId) {
return StockDTO.builder()
.productId(productId)
.productName("飞翔机械键盘 Pro")
.stock(100)
.build();
}
}
订单服务消费者——Feign 接口:
@FeignClient(name = "FEIXIANG-INVENTORY-SERVICE")
public interface InventoryClient {
@GetMapping("/inventory/{productId}")
StockDTO getStock(@PathVariable("productId") Long productId);
}
订单服务消费端调用:
@RestController
public class OrderController {
@Autowired
private InventoryClient inventoryClient;
@GetMapping("/order/check-stock/{productId}")
public StockDTO checkStock(@PathVariable Long productId) {
// 像调用本地方法一样调用远程服务
return inventoryClient.getStock(productId);
}
}
示例二:POST 请求 + 请求体(创建订单)
@FeignClient(name = "FEIXIANG-ORDER-SERVICE")
public interface OrderClient {
@PostMapping("/orders")
OrderDTO createOrder(@RequestBody OrderRequest request);
@GetMapping("/orders/{orderId}")
OrderDTO getOrder(@PathVariable("orderId") String orderId);
}
示例三:多参数 + 请求头
@FeignClient(name = "FEIXIANG-USER-SERVICE")
public interface UserClient {
@GetMapping("/users/{userId}/orders")
List<OrderDTO> getUserOrders(
@PathVariable("userId") Long userId,
@RequestParam("status") String status,
@RequestHeader("X-Trace-Id") String traceId
);
}
启动类配置
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.feixiang.order.client")
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
Feign 日志配置
开发阶段建议开启 FULL 级别日志,便于调试:
logging:
level:
com.feixiang.order.client.InventoryClient: DEBUG
feign:
client:
config:
default:
loggerLevel: FULL # NONE/BASIC/HEADERS/FULL
FULL 级别日志输出示例:
[InventoryClient#getStock] ---> GET http://FEIXIANG-INVENTORY-SERVICE/inventory/1001
[InventoryClient#getStock] ---> END HTTP (0-byte body)
[InventoryClient#getStock] <--- HTTP/1.1 200 (145ms)
[InventoryClient#getStock] {"productId":1001,"productName":"飞翔机械键盘 Pro","stock":100}
[InventoryClient#getStock] <--- END HTTP (86-byte body)
易错场景
1. @PathVariable 未指定 value 导致启动失败
// ❌ 错误:缺少 value 属性
@GetMapping("/inventory/{productId}")
StockDTO getStock(@PathVariable Long productId);
// ✅ 正确:显式指定 value
@GetMapping("/inventory/{productId}")
StockDTO getStock(@PathVariable("productId") Long productId);
原因:Java 8 编译时默认不保留方法参数名,Feign 无法推断 @PathVariable 对应的 URL 变量名。
2. Feign 接口返回类型不能是基本类型
// ❌ 错误
@GetMapping("/inventory/{productId}")
int getStock(@PathVariable Long productId);
// ✅ 正确:使用包装类型
@GetMapping("/inventory/{productId}")
Integer getStock(@PathVariable Long productId);
3. 多个 @RequestBody 参数
// ❌ 错误:Feign 不支持多个 @RequestBody
@PostMapping("/orders")
OrderDTO createOrder(@RequestBody UserDTO user, @RequestBody ProductDTO product);
// ✅ 正确:合并为一个 DTO
@PostMapping("/orders")
OrderDTO createOrder(@RequestBody OrderRequest request);
4. 继承接口导致路径重复
// ❌ 错误:如果父接口也有 @RequestMapping,路径可能重复
@FeignClient("FEIXIANG-INVENTORY-SERVICE")
public interface InventoryClient extends BaseClient { ... }
// ✅ 正确:Feign 接口不要继承带 @RequestMapping 的接口
面试考点
Feign 的工作原理?
Feign 通过 JDK 动态代理,在运行时为
@FeignClient标注的接口生成实现类。当调用接口方法时,代理对象通过 Contract 解析注解生成请求模板,Encoder 将参数编码为 HTTP 请求,HTTP Client 发起真实请求,Decoder 将响应解码为返回类型。整个过程对调用方透明。
Feign 支持哪些 HTTP 客户端?
默认使用
java.net.HttpURLConnection(无连接池,性能差)。生产环境推荐替换为:
- Apache HttpClient:成熟稳定,连接池支持
- OkHttp:轻量高性能,支持 HTTP/2
切换方法:引入对应 Starter 或手动配置
feign.httpclient.enabled=true/feign.okhttp.enabled=true
Feign 如何与 LoadBalancer 集成?
Feign 中配置的服务名(如
FEIXIANG-INVENTORY-SERVICE)会自动通过 LoadBalancer 解析为实际 IP:Port。如果项目中同时有 LoadBalancer 和 Feign,@FeignClient的 name 就是服务名,Feign 内置的 LoadBalancer 支持会自动替换 URL。
小结
Feign 让微服务间的 HTTP 调用变得像调用本地方法一样简单。通过声明式注解、动态代理、编解码器,Feign 消除了模板代码。但它只是让调用写得更优雅,并没有解决调用失败的问题——下一节 OpenFeign 将结合熔断器,在声明式调用的基础上加入容错能力。