禁止 Feign!我们为什么自研 InternalServiceClient

0 阅读8分钟

@[toc]

禁止 Feign!我们为什么自研 InternalServiceClient

微服务跨服务调用,90% 的人用 Feign——但我们选择不。


一、微服务跨服务调用,为什么不用 Feign?

国内做 Spring Cloud 微服务,跨服务调用几乎只有一个答案:Feign。

OpenFeign 确实是经典方案:声明式接口、自动序列化、集成 Ribbon 负载均衡。但用久了就会发现,它解决了一些问题的同时,引入了更多新问题。

在 MetaLite 的设计过程中,我们做了一个决定:禁止使用 Feign,自研 InternalServiceClient

这个决定在团队内部也争论了很久。毕竟 Feign 是 Spring Cloud 的标配,不用它意味着要自己写一套调用框架。但经过几个项目的踩坑,我们确信这是一个正确的选择。

这篇文章,我把 Feign 的痛点、InternalServiceClient 的设计思路、以及两者的代码对比说清楚。看完之后,你可以自己判断。


二、Feign 的痛点

2.1 上下文传递困难

微服务调用链中,有很多上下文信息需要在服务间传递:

  • TraceId:全链路追踪,排查问题刚需
  • 用户 ID:下游服务需要知道是谁发起的请求
  • AppId:调用方标识,用于权限控制和审计
  • Seata XID:分布式事务的 transaction ID

用 Feign 时,这些信息需要通过 RequestInterceptor 手动注入到 HTTP Header 中:

@Component
public class FeignHeaderInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("traceId", ThreadContext.getTraceId());
        template.header("userId", ThreadContext.getLoginUserId());
        template.header("appId", ThreadContext.getAppId());
        template.header("X-Seata-XID", ThreadContext.getSeataXid());
    }
}

看似没问题,但有两个隐患:

  • 隐式依赖:Feign 的 Header 传递是全局拦截器做的,某个服务忘了配拦截器,上下文就断了,排查困难
  • 传递逻辑分散:TraceId、用户 ID、XID 各自在不同的拦截器或拦截逻辑中处理,缺少统一入口

2.2 超时控制不灵活

Feign 的超时配置粒度很粗。通常是在 application.yml 中全局设置:

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 10000

问题是:不同调用的超时需求差异很大

  • 查询用户基本信息:50ms 就够了
  • 生成报表导出:可能需要 30 秒
  • 批量同步数据:可能要 1 分钟

用 Feign 要么全局改(影响其他调用),要么每个 Client 单独配配置类(代码膨胀)。

更严重的是,在微服务调用链中,如果 A → B → C 每层都用 10 秒超时,最终用户可能要等 30 秒才拿到响应。这就是超时累积效应——调用链越长,最终响应越慢。

2.3 降级处理复杂

Feign 的降级(fallback)依赖 Hystrix 或 Resilience4j:

@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    @GetMapping("/api/user/info")
    Resp<UserDto> getUserInfo(@RequestParam String userId);
}

@Component
public class UserClientFallback implements UserClient {
    @Override
    public Resp<UserDto> getUserInfo(String userId) {
        return Resp.error("用户服务暂时不可用");
    }
}

每个接口都要写一个 Fallback 实现类。服务越多,Fallback 类越多,代码量直线上升。

而且,Fallback 是接口级别的,不是调用级别的。同一个接口,有的调用方需要降级,有的不需要,但 Feign 只能配一个全局策略。

2.4 接口定义和实现割裂

Feign 要求调用方定义接口:

@FeignClient(name = "user-service")
public interface UserClient {
    @PostMapping("/api/admin/sysUser/getInfo")
    Resp<UserDto> getUserInfo(@RequestBody InternalReq req);
}

但接口的实际实现是被调用方的 Controller。当被调用方改了接口路径、参数类型、返回格式时,调用方的 Feign 接口不会有任何编译期提示——只有在运行时调用失败才会发现问题

对于服务数量多、迭代频繁的项目,这种割裂感会非常痛苦。


三、InternalServiceClient 的设计

3.1 核心设计理念

MetaLite 的 InternalServiceClient 设计哲学很明确:一个统一的调用入口,自动处理所有横切关注点

public class InternalServiceClient {

    public void callOneInstance(RpcRequest rpcRequest) { }

    public <T> Resp<T> callOneInstanceRtnData(RpcRequest rpcRequest, Class<T> respDataType) { }

    public <T> Resp<List<T>> callOneInstanceRtnListData(RpcRequest rpcRequest, Class<T> respDataType) { }

    public <E> Resp<PageResultDto<E>> callOneInstanceRtnPageData(RpcRequest rpcRequest, Class<E> respDataType) { }
}

只有 4 个核心方法,覆盖所有调用场景:

方法用途
callOneInstance调用单个实例,无返回值
callOneInstanceRtnData调用单个实例,返回单个对象
callOneInstanceRtnListData调用单个实例,返回集合
callOneInstanceRtnPageData调用单个实例,返回分页数据

3.2 上下文自动传递

这是 InternalServiceClient 最核心的能力之一。在 checkAndFillRpcRequest 方法中,自动注入所有上下文:

private RpcRequest checkAndFillRpcRequest(RpcRequest rpcRequest) {
    // 构建内部请求头,自动传递全链路上下文
    Map<String, String> headers = new HashMap<>();
    headers.put(TRACE_ID, ThreadContext.getTraceId());
    headers.put(LOGIN_USER_ID, ThreadContext.getLoginUserId());
    headers.put(APP_ID, ThreadContext.getAppId());
    headers.put(SEATA_XID, ThreadContext.getSeataXid());

    if (rpcRequest.getHeaders() == null) {
        rpcRequest.setHeaders(headers);
    } else {
        rpcRequest.getHeaders().putAll(headers);
    }
    return rpcRequest;
}

开发者不需要关心 Header 怎么传、TraceId 怎么带过去、Seata XID 怎么跨服务传播——只要用 InternalServiceClient,这些全部自动处理

3.3 超时逐层递减

这是 InternalServiceClient 区别于 Feign 的关键设计。在 checkAndFillRpcRequest 中:

// 超时时间逐层递减
int timeoutMillis = rpcRequest.getTimeoutMillis();
timeoutMillis = timeoutMillis <= 0 ? API_REQUEST_TIMEOUT_MILLIS : timeoutMillis - API_REQUEST_TIMEOUT_MILLIS_MINUS;
rpcRequest.setTimeoutMillis(timeoutMillis);

什么意思?

A → B → C → D

A 发起调用时超时 = 5000ms
B 收到调用时超时 = 5000 - 500 = 4500ms
C 收到调用时超时 = 4500 - 500 = 4000ms
D 收到调用时超时 = 4000 - 500 = 3500ms

每经过一层,超时时间自动递减。这保证了:

  • 调用链越长,下游超时越短,避免下游服务长时间等待
  • 最终用户等待时间有上限,不会出现调用链累积导致的超长等待
  • 下游服务更容易快速失败,减少雪崩风险

3.4 基于 Nacos 服务发现

InternalServiceClient 底层通过 Nacos 做服务发现,调用时指定服务名即可:

RpcRequest rpcRequest = RpcRequest.builder()
        .provider("user-service")          // 服务名,Nacos 自动发现
        .endpoint("/api/admin/sysUser/getInfo")
        .param(internalReq)
        .rpcMode(RpcModeEnum.HTTP)
        .build();

Resp<UserDto> resp = internalServiceClient.callOneInstanceRtnData(rpcRequest, UserDto.class);

不需要 Feign 那样的 @FeignClient(name = "user-service") 注解绑定服务名。服务名在每次调用时显式指定,清晰且可控。

3.5 响应类型安全

Feign 返回的是接口定义的类型,但类型转换在运行时才校验。InternalServiceClient 要求在调用时显式指定响应数据类型:

// 单个对象
Resp<UserDto> resp = internalServiceClient.callOneInstanceRtnData(rpcRequest, UserDto.class);

// 集合
Resp<List<OrderDto>> resp = internalServiceClient.callOneInstanceRtnListData(rpcRequest, OrderDto.class);

// 分页
Resp<PageResultDto<UserDto>> resp = internalServiceClient.callOneInstanceRtnPageData(rpcRequest, UserDto.class);

内部会严格校验类型:

paramCheck(!Collection.class.isAssignableFrom(respDataType), "respDataType", "不能是集合类型");

如果类型不匹配,在调用方就会立即报错,而不是在下游服务里静默失败。

3.6 切面链增强

InternalServiceClient 的所有方法都被 ApiCallAspect 拦截:

@Around("execution(public * com.metalite.rpc.InternalServiceClient.call*(..))")
private Object aroundBaseInternalServiceClient(ProceedingJoinPoint pjp) throws Throwable {
    // 统一的日志、监控、异常处理
}

这意味着每次 RPC 调用都会自动经过:

  • 日志记录:调用方、被调方、耗时、响应码
  • 异常处理:统一封装为 Resp 格式
  • 监控埋点:调用成功率、延迟分布

开发者不需要手动加 try-catch、打日志、做监控——全部自动完成。


四、代码对比

4.1 Feign 方式

// 1. 定义 Feign 接口
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    @PostMapping("/api/admin/sysUser/getInfo")
    Resp<UserDto> getUserInfo(@RequestBody InternalReq req);
}

// 2. 写 Fallback 实现
@Component
public class UserClientFallback implements UserClient {
    @Override
    public Resp<UserDto> getUserInfo(InternalReq req) {
        return Resp.error("用户服务暂时不可用");
    }
}

// 3. 写 RequestInterceptor 传递上下文
@Component
public class FeignHeaderInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("traceId", ThreadContext.getTraceId());
        template.header("userId", ThreadContext.getLoginUserId());
        // ... 每个 Header 都要手动加
    }
}

// 4. 调用方使用
@Resource
private UserClient userClient;

public Resp<UserDto> getUser(String userId) {
    InternalReq req = new InternalReq();
    req.putParam("userId", userId);
    return userClient.getUserInfo(req);  // 没有超时递减
}

4.2 InternalServiceClient 方式

@Resource
private InternalServiceClient internalServiceClient;

public Resp<UserDto> getUser(String userId) {
    InternalReq req = new InternalReq();
    req.putParam("userId", userId);

    RpcRequest rpcRequest = RpcRequest.builder()
            .provider("user-service")
            .endpoint("/api/admin/sysUser/getInfo")
            .param(req)
            .rpcMode(RpcModeEnum.HTTP)
            .build();

    // 一行调用:上下文自动传、超时自动减、响应自动转换
    return internalServiceClient.callOneInstanceRtnData(rpcRequest, UserDto.class);
}

对比一下:

维度FeignInternalServiceClient
接口定义需要定义 Feign 接口 + Fallback 类不需要,直接构造 RpcRequest
上下文传递需要手动写 RequestInterceptor自动注入
超时控制全局配置或每个 Client 单独配逐层自动递减
降级处理每个接口写 Fallback 实现响应体统一处理
类型安全运行时校验调用时显式指定
监控日志需要额外配置切面自动处理

五、总结设计哲学

禁止 Feign 不是因为我们觉得 Feign 不好,而是因为 Feign 解决的是"能不能调"的问题,但微服务更需要的是"调得好"的问题

InternalServiceClient 的设计哲学:

  • 约定优于配置:上下文传递、超时递减、切面监控全部自动,不需要开发者操心
  • 显式优于隐式:服务名、端点、响应类型每次调用都显式指定,不留隐患
  • 统一入口优于分散定义:一个 Client 覆盖所有调用场景,不需要为每个服务写单独的接口

这不是对 Feign 的否定,而是对微服务调用本质的另一种理解。

InternalServiceClient 的这套设计哲学,贯穿了整个 MetaLite 框架——约定优于配置、显式优于隐式、统一入口优于分散定义。


框架简介:元界 MetaLite — 下一代企业级 Java 微服务技术底座

作者简介:基于 Spring 体系 15 年企业级开发经验,专注于通过企业级生产环境落地的工程思维和架构思想打造下一代Java 微服务技术底座

完整文档与源码:Gitee 搜索 MetaLite (gitee.com/MetaLite)