Feign 传参与继承特性
你有没有想过:如果多个微服务都要调用同一批接口,每个服务都写一遍 @FeignClient 定义,会不会很麻烦?
Feign 的继承特性和高级传参,让你的代码复用达到最大化。
从一个问题开始
假设你有这样的场景:
┌─────────────────────────────────────────────────────────┐
│ 用户服务暴露的 API │
│ │
│ GET /user/{id} → 获取用户信息 │
│ POST /user → 创建用户 │
│ GET /user/list → 获取用户列表 │
│ PUT /user/{id} → 更新用户 │
│ DELETE /user/{id} → 删除用户 │
│ │
└─────────────────────────────────────────────────────────┘
订单服务要调用这些接口
支付服务也要调用这些接口
商品服务也要调用这些接口每个服务都写一遍 @FeignClient?代码重复,维护噩梦。
Feign 的继承特性,就是来解决这个问题的。
继承特性
1. 定义公共接口
创建一个独立的 API 模块,定义公共的 Feign 接口:
java
// user-api 模块
public interface UserApi {
@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);
@PostMapping("/user")
User createUser(@RequestBody UserCreateRequest request);
@GetMapping("/user/list")
List<User> getUsers(@RequestParam("ids") List<Long> ids);
@PutMapping("/user/{id}")
User updateUser(@PathVariable("id") Long id,
@RequestBody UserUpdateRequest request);
@DeleteMapping("/user/{id}")
void deleteUser(@PathVariable("id") Long id);
}2. 服务端实现接口
用户服务实现这个接口,保证 API 定义和 Controller 一致:
java
@RestController
@RequestMapping("/user")
public class UserController implements UserApi {
@Autowired
private UserService userService;
@Override
public User getUser(Long id) {
return userService.getById(id);
}
@Override
public User createUser(UserCreateRequest request) {
return userService.create(request);
}
// ... 其他方法
}3. 客户端继承接口
订单服务直接继承 UserApi:
java
@FeignClient(name = "user-service")
public interface OrderUserClient extends UserApi {
// 无需定义任何方法,直接继承
}支付服务也一样:
java
@FeignClient(name = "user-service")
public interface PaymentUserClient extends UserApi {
// 无需定义任何方法,直接继承
}项目结构
├── user-api # 公共 API 模块
│ └── src/main/java
│ └── com.example.api
│ └── UserApi.java
├── user-service # 用户服务
│ └── src/main/java
│ └── com.example.user
│ └── UserController implements UserApi
├── order-service # 订单服务
│ └── src/main/java
│ └── com.example.order
│ └── OrderUserClient extends UserApi
└── payment-service # 支付服务
└── src/main/java
└── com.example.payment
└── PaymentUserClient extends UserApiMaven 依赖
xml
<!-- user-api 模块 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>user-api</artifactId>
<version>1.0.0</version>
</dependency>继承的注意事项
继承的局限性
java
// 不推荐:@FeignClient 在继承接口上
@FeignClient(name = "user-service")
public interface UserApi { // ERROR: 不要在这里加注解
// ...
}
// 推荐:@FeignClient 在实现类上
public interface UserApi {
// ...
}
@FeignClient(name = "user-service")
public interface OrderUserClient extends UserApi { // OK
// ...
}路径前缀问题
如果不同服务需要不同的 path,使用空 path:
java
public interface UserApi {
@RequestMapping("/api/users") // 使用 @RequestMapping 指定完整路径
@GetMapping("/{id}")
User getUser(@PathVariable("id") Long id);
}复杂传参
1. 路径参数 @PathVariable
java
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/{id}/detail/{type}")
UserDetail getUserDetail(@PathVariable("id") Long id,
@PathVariable("type") String type);
// 多个路径参数
@GetMapping("/org/{orgId}/user/{userId}")
OrgUser getOrgUser(@PathVariable("orgId") Long orgId,
@PathVariable("userId") Long userId);
}2. 查询参数 @RequestParam
java
@FeignClient(name = "user-service")
public interface UserClient {
// 单个参数
@GetMapping("/user/search")
List<User> searchByName(@RequestParam("name") String name);
// 多个参数
@GetMapping("/user/search")
List<User> searchUsers(@RequestParam("name") String name,
@RequestParam("age") Integer age,
@RequestParam("city") String city);
// 参数集合
@GetMapping("/user/batch")
List<User> getUsersByIds(@RequestParam("ids") List<Long> ids);
// Map 参数
@GetMapping("/user/search")
List<User> searchUsers(@RequestParam Map<String, Object> params);
}3. 请求体 @RequestBody
java
@FeignClient(name = "user-service")
public interface UserClient {
// JSON 请求体
@PostMapping("/user/create")
User createUser(@RequestBody UserCreateRequest request);
// 复杂嵌套对象
@PostMapping("/order/create")
Order createOrder(@RequestBody OrderCreateRequest request);
}java
@Data
public class OrderCreateRequest {
private Long userId;
private List<OrderItem> items;
private PaymentInfo payment;
}4. Header 参数 @RequestHeader
java
@FeignClient(name = "user-service")
public interface UserClient {
// 单个 Header
@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id,
@RequestHeader("Authorization") String token);
// 多个 Header
@PostMapping("/user")
User createUser(@RequestBody UserCreateRequest request,
@RequestHeader("Authorization") String token,
@RequestHeader("X-Request-Id") String requestId,
@RequestHeader("Accept-Language") String language);
}5. POJO 参数(不推荐)
Spring Cloud 2.x 之后,支持直接使用 POJO 作为查询参数:
java
// 不推荐:查询参数用 POJO
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/user/search")
List<User> searchUsers(UserQuery query); // 会转换为查询参数
}推荐:明确使用 @RequestParam,便于调试和维护。
请求拦截器
全局拦截器
java
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Autowired
private TokenService tokenService;
@Override
public void apply(RequestTemplate template) {
// 添加认证 Token
String token = tokenService.getToken();
if (StringUtils.hasText(token)) {
template.header("Authorization", "Bearer " + token);
}
// 添加 TraceId
template.header("X-Trace-Id", UUID.randomUUID().toString());
// 添加租户信息
template.header("X-Tenant-Id", TenantContext.getTenantId());
}
}针对特定客户端的拦截器
java
@Configuration
public class FeignInterceptorConfig {
@Bean
public FeignInterceptor userServiceInterceptor() {
return new FeignInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 针对 user-service 的特殊处理
template.header("X-User-Service-Token", "special-token");
}
};
}
}
@FeignClient(name = "user-service", configuration = FeignInterceptorConfig.class)
public interface UserClient {
// ...
}响应拦截器
java
@Component
public class FeignResponseInterceptor implements ResponseInterceptor {
@Override
public void apply(ResponseTemplate template) {
// 在响应前处理
}
@Override
public Map<String, Collection<String>> resolveHeaders(
RequestTemplate template,
Map<String, Collection<String>> headers) {
// 可以修改响应头
return headers;
}
}请求重试
配置重试
yaml
spring:
cloud:
openfeign:
client:
config:
default:
retryer: feign.retryer.Retryer.Default # 启用重试,默认 5 次
user-service:
retryer: MyRetryer # 自定义重试器自定义重试器
java
public class MyRetryer implements Retryer {
private final int maxAttempts = 3;
private final long period = 100;
private final long maxPeriod = 1000;
private int attempt = 1;
@Override
public void continueOrPropagate(RetryableException e) {
if (attempt++ >= maxAttempts) {
throw e;
}
long interval = (long) Math.min(
expBackoff(attempt, period), maxPeriod);
try {
Thread.sleep(interval);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
private long expBackoff(int attempt, long period) {
return period * (long) Math.pow(1.5, attempt - 1);
}
@Override
public Retryer clone() {
return new MyRetryer();
}
}错误处理
自定义 ErrorDecoder
java
@Component
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
try {
// 读取错误响应体
String body = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
if (response.status() == 400) {
return new BadRequestException(body);
}
if (response.status() == 401) {
return new UnauthorizedException(body);
}
if (response.status() == 404) {
return new NotFoundException(body);
}
if (response.status() == 500) {
return new InternalServerException(body);
}
return new FeignException(
response.status(),
body,
response.headers(),
body.getBytes(StandardCharsets.UTF_8)
);
} catch (IOException e) {
return new FeignException.InternalError(e.getMessage());
}
}
}@RequestLine vs @RequestMapping
Feign 提供了两种定义请求的方式:
@RequestLine(原生 Feign)
java
public interface UserClient {
@RequestLine("GET /user/{id}")
User getUser(@Param("id") Long id);
@RequestLine("POST /user")
User createUser(@Param("name") String name, @Param("email") String email);
}@RequestMapping(Spring Cloud 扩展,推荐)
java
public interface UserClient {
@RequestMapping(method = RequestMethod.GET, value = "/user/{id}")
User getUser(@PathVariable("id") Long id);
}推荐使用 @RequestMapping,因为它与 Spring MVC 注解兼容,IDE 支持更好。
面试高频问题
Q:Feign 继承特性有什么优缺点?
A:优点——代码复用,减少重复定义,API 变更时只需修改一处。缺点——增加模块耦合度,API 模块和客户端模块强依赖;可能导致不必要的依赖传递。
Q:Feign 如何传递复杂对象参数?
A:GET 请求使用 @RequestParam + Map,POST 请求使用 @RequestBody。注意 GET 请求不支持 @RequestBody。
Q:Feign 的请求拦截器有什么用?
A:可以统一添加认证信息(Token)、TraceId、租户信息等,避免在每个方法上手动添加 Header。
Q:Feign 如何处理 404 等 HTTP 错误?
A:默认抛出 FeignException。可以通过自定义 ErrorDecoder 将 HTTP 错误码转换为业务异常,便于调用方处理。
总结
Feign 的高级特性让微服务调用更加灵活:
- 继承特性:公共接口复用,避免重复定义
- 复杂传参:@PathVariable、@RequestParam、@RequestBody 灵活组合
- 拦截器:统一处理认证、追踪等横切关注点
- 重试机制:自动处理临时故障
- 错误处理:自定义错误解码器
用好这些高级特性,可以让 Feign 代码既简洁又健壮。
