返回资源中心

微服务间调用最佳实践 - Client SDK

讲清楚企业里怎么把"服务间调用"封装成 client-sdk,它长什么样、每一层干什么、以及相比上一课的常规 Feign 有哪些实打实的优势。小红学堂更新于 2026年8月28日234 次阅读

视频里结合真实项目 xiangmusystem-client-sdk 模块讲解。 本课目标:讲清楚企业里怎么把"服务间调用"封装成 client-sdk,它长什么样、每一层干什么、以及相比上一课的常规 Feign 有哪些实打实的优势。


一、回顾:常规 Feign 的四个痛点

上一课我们用 Feign 实现了服务间调用。但在多服务、多团队的真实项目里,"每个调用方各自写 @FeignClient"会带来一堆重复和不一致:

痛点说明
重复定义每个调用方都自己写一遍 Feign 接口和 DTO
DTO 对不上各自定义返回对象,提供方改字段时调用方不同步
解包重复真实返回是 ApiResponse<T>,每个调用方都要自己解一层
降级重复服务挂了的容错逻辑,每个调用方都得各写一遍

核心思路转变:

与其让"调用方"各自摸索怎么调我,不如由服务提供方把"怎么调用我"统一封装成一个 SDK,发布出去,别人依赖即用。 这个 SDK 就叫 client-sdk——谁的服务,谁维护自己的 client-sdk。


二、client-sdk 长什么样:标准分层结构

真实的 system-client-sdk(系统服务对外的 client-sdk)是这样分层的:

角色谁会用
api/接口层:纯接口,定义"能调什么能力"消费方注入这个
api/impl/实现层:调 Feign、解 ApiResponse、降级、记日志内部,自动装配
feign/Feign 原始客户端@FeignClient内部,禁止消费方直接注入
dto/共享 DTO:提供方和消费方共用的契约对象双方共用
config/自动装配 + 拦截器内部,依赖即生效

真实 pom(system-client-sdk/pom.xml,很轻量):

<artifactId>system-client-sdk</artifactId>
<description>系统服务-对外 SDK:Feign 客户端 + 共享 DTO(供其它服务依赖)</description>
<dependencies>
    <dependency><groupId>com.xiangmu</groupId><artifactId>xiangmu-common</artifactId></dependency>
    <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
    <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>

下面逐层看真实代码,理解每层为什么存在。


三、逐层看真实代码

3.1 feign 层:原始 Feign 客户端(内部,不对外)

这一层就是上一课讲的 @FeignClient,对应 system 的内部接口。但注意注释里的铁律:禁止调用方直接注入它

@FeignClient(
        name = "system-service",
        contextId = "systemAuthClient",
        url = "${xiangmu.client.system-url:http://localhost:8081}",
        path = "/api/system/internal/auth")
public interface SystemAuthClient {

    @GetMapping("/active")
    ApiResponse<Boolean> isTokenActive(@RequestHeader("X-Access-Token") String accessToken);
}

注意它返回的是 ApiResponse<Boolean>——带着统一响应外壳。如果让消费方直接用它,每个消费方都得自己拆这层壳、自己处理失败。所以我们在它外面再包一层。

3.2 api 接口层:消费方真正依赖的"门面"

public interface SystemAuthApi {
    /**
     * 校验访问令牌是否有效(未被登出)。
     * 降级策略 Fail-closed:system 不可用时返回 false(视为无效)。
     */
    boolean isTokenActive(String accessToken);
}

看区别:接口层返回的是干净的 boolean,不是 ApiResponse<Boolean>消费方依赖这个接口,根本看不到 Feign、看不到响应外壳、不用关心降级——它只知道"我要校验令牌,给我个 true/false"。

3.3 impl 实现层:把"脏活"集中做掉

接口层和 Feign 层之间,夹着一个实现层。它干三件"脏活":调 Feign + 解 ApiResponse + 失败降级 + 记日志

@Slf4j
@RequiredArgsConstructor
public class SystemAuthApiImpl implements SystemAuthApi {

    private final SystemAuthClient systemAuthClient;   // 注入原始 Feign 客户端

    @Override
    public boolean isTokenActive(String accessToken) {
        try {
            ApiResponse<Boolean> resp = systemAuthClient.isTokenActive(accessToken);
            if (resp != null && resp.isSuccess() && resp.getData() != null) {
                return resp.getData();                 // ① 解包:从 ApiResponse 取出 boolean
            }
            log.warn("校验令牌返回异常,按无效处理: {}", resp == null ? "null" : resp.getMessage());
            return false;                              // ② 返回体异常也按无效(Fail-closed)
        } catch (Exception e) {
            log.error("调用 system 校验令牌失败,按无效处理(Fail-closed)", e);
            return false;                              // ③ 远程挂了 → 降级,安全优先
        }
    }
}

这一层就是 client-sdk 的价值核心

  • 解包:把 ApiResponse<Boolean> 解成 boolean,消费方拿到干净结果;
  • 降级容错:远程失败/超时统一兜底(这里是 Fail-closed 返回 false),消费方不用写一行 try-catch;
  • 日志:调用失败统一记录,排查问题有据可查。

这些逻辑只在 SDK 里写一次,所有消费方共享。对比常规做法"每个调用方各写一遍",高下立判。

3.4 config 层:自动装配,让消费方零样板

消费方凭什么注入 SystemAuthApi 就能用?靠自动配置(和公共模块那课的机制一样)。

@AutoConfiguration
@ConditionalOnClass(feign.Feign.class)              // 没引 Feign 的模块不会误装配
public class SystemClientAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public SystemAuthApi systemAuthApi(SystemAuthClient client) {
        return new SystemAuthApiImpl(client);        // 把实现注册成 Bean
    }

    @Bean
    @ConditionalOnMissingBean
    public RequestInterceptor feignTenantInterceptor() {
        return new FeignTenantInterceptor();         // 注册租户透传拦截器(见 3.5)
    }
    // ...SystemUserApi / SystemRbacApi 同理
}

配合 META-INF/spring/...AutoConfiguration.imports 登记这个配置类——消费方只要依赖 SDK,这些 Bean 就自动装好了。

注意 SystemAuthApiImpl没有 @Component,而是由自动配置显式 new 出来注册:保证"只有引入了 client-sdk 的服务"才装配,不污染别人。

3.5 拦截器:上下文透传,对消费方透明

跨服务调用还有个隐藏需求:调用方的"身份/租户"信息要带给下游FeignTenantInterceptor 自动把当前线程的租户、用户 ID 写进请求头:

public class FeignTenantInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        Long tenantId = TenantContext.getTenantId();
        Long userId   = TenantContext.getUserId();
        if (tenantId != null) template.header("X-Tenant-Id", String.valueOf(tenantId));
        if (userId != null)   template.header("X-User-Id",  String.valueOf(userId));
    }
}

业务代码完全不用管这件事——只要走这个 client-sdk 调别的服务,租户/用户身份就自动带过去了,下游服务也能正确做数据隔离。这种"对使用方透明的横切能力",正是 SDK 封装的威力。


四、消费方怎么用:两步,零样板

以网关调用 system 校验令牌为例(真实场景):

第一步:pom 依赖 client-sdk + 启动类启用 Feign:

<dependency>
    <groupId>com.xiangmu</groupId>
    <artifactId>system-client-sdk</artifactId>
</dependency>
@SpringBootApplication
@EnableFeignClients(basePackages = "com.xiangmu.system.client.feign")
public class GatewayApplication { ... }

第二步:直接注入接口层,调用:

// 网关里直接注入接口层,不碰 Feign、不碰 ApiResponse、不写降级
private final SystemAuthApi systemAuthApi;

boolean active = systemAuthApi.isTokenActive(token);   // 干净!

对比上一课:消费方不写 @FeignClient、不定义 DTO、不解 ApiResponse、不写 try-catch 降级——这些全在 SDK 里做好了。这就是"依赖即用"。


五、client-sdk 到底有哪些优势

这是本课的重点。把 client-sdk 相对常规 Feign 的优势列清楚:

#优势常规做法client-sdk
1接口与实现隔离调用方直接用 Feign调用方只依赖纯接口 xxxApi,看不到 Feign 细节,可替换实现
2统一降级容错每个调用方各写 try-catchimpl 层集中做(Fail-closed 等),一处维护
3统一解包响应每个调用方各解 ApiResponseimpl 层解好,返回干净业务类型
4共享 DTO 契约各自定义 DTO,易对不上提供方和消费方共用同一套 DTO,字段天然一致
5自动装配零样板调用方各种配置依赖 SDK + @EnableFeignClients 即注入即用
6上下文透传透明调用方手动塞请求头拦截器自动透传租户/用户身份,业务无感
7提供方维护、消费方零成本接口变更各调用方各自改提供方改接口随 SDK 一起发布,消费方升版本即可
8调用入口收敛散落各处"谁能调我、怎么调"集中在一个 SDK,清晰可控

一句话:client-sdk 把"调用一个服务"的所有麻烦事(Feign、DTO、解包、降级、透传)集中封装一次,所有调用方零成本复用。


六、设计思想:这其实就是「依赖倒置原则 DIP」

学到这里,如果你学过设计原则,应该会有种似曾相识的感觉——client-sdk 的 api/impl 分层,本质就是「依赖倒置原则」(Dependency Inversion Principle, DIP) 的教科书应用。

6.1 依赖的箭头被"倒置"了

DIP 的定义是:高层模块不依赖低层模块,两者都依赖抽象;抽象不依赖细节,细节依赖抽象。

光看定义有点抽象,我们把"倒置前"和"倒置后"两张图摆一起,对比就清楚了。

① 倒置前(上一课的常规做法):消费方直接依赖 Feign 这个低层细节,箭头一路"高层 → 低层"往下指:

问题:高层被低层细节"绑死"了——Feign 怎么变、ApiResponse 怎么解、失败怎么降级,消费方全得知道。

② 倒置后(client-sdk 的做法):中间插入一个抽象接口 SystemAuthApi高层和低层都反过来指向它

对比两张图看箭头方向:

  • 倒置前:消费方 → Feign(高层的箭头直直指向低层细节);
  • 倒置后:消费方 → 接口 ← 实现(低层那条箭头被掉了个头,从"被依赖"变成"反过来依赖/实现抽象")。

低层对抽象的依赖箭头被"调转方向"——这就是"倒置/反转"这个名字的由来,也正是第五节优势 #1「接口与实现隔离」的理论名字。

6.2 DIP(思想)和 Spring IoC(机制)的关系

很多人会把这跟 Spring 的"控制反转/依赖注入"混在一起,这里分清楚:

概念是什么在 client-sdk 里
依赖倒置 DIP设计原则:面向接口编程我设计成"消费方只认 SystemAuthApi 接口"
控制反转 IoC / 依赖注入 DI框架机制:容器创建并注入对象SystemClientAutoConfigurationImpl 注册成 Bean,构造注入给消费方

一句话:client-sdk 用「依赖倒置」做设计,借「Spring 的 IoC/DI」来落地。 前者是思想,后者是把思想变成现实的工具,两者搭配。

6.3 DIP 的威力:实现可替换

因为消费方只依赖接口,哪天底层把 Feign 换成 gRPC、或换成 Spring 6 的 RestClient,只要改 Impl、接口不变,所有消费方零改动。 这种"实现可替换"正是依赖倒置的核心价值——而常规做法里消费方直接绑死 @FeignClient,就享受不到。

💡 顺带一提:client-sdk 还叠了两个经典设计模式——api 接口层屏蔽 Feign/解包/降级的复杂性,是门面模式 (Facade)implApiResponse<Boolean> 适配成干净的 boolean,是适配器模式 (Adapter)。但主线就是 DIP。


七、client-sdk 和 common-sdk 的区别

别把两种 SDK 搞混(呼应《公共模块封装》那课):

xiangmu-common(公共 SDK)xxx-client-sdk(调用 SDK)
解决什么各服务自己内部要用的通用能力(统一响应、异常、工具、JWT)调用别的服务这件事
谁维护平台/架构组统一维护被调用的那个服务自己维护
一个项目有几个1 个每个对外提供能力的服务各有 1 个

记忆:common-sdk 是"我自己用的工具箱";client-sdk 是"我发给别人的、怎么调用我的说明书 + 现成客户端"。


八、小结

七句话带走:

  1. client-sdk 由服务提供方封装:谁的服务,谁维护"怎么调我"。
  2. 标准分层api 接口层(对外)、impl 实现层(降级+解包+日志)、feign 原始客户端(内部)、dto 共享契约、config 自动装配。
  3. 铁律:消费方只依赖接口层,绝不直接用 @FeignClient
  4. 脏活集中做一次:解包、降级、日志、上下文透传都在 SDK 里,所有调用方复用。
  5. 消费方零样板:依赖 SDK + @EnableFeignClients + 注入接口即可,不写 Feign/DTO/降级。
  6. 本质是依赖倒置原则(DIP):面向接口编程、实现可替换;借 Spring IoC/DI 落地。
  7. 和 common-sdk 区分:common 是"自己用的工具",client-sdk 是"给别人调我的客户端"。