视频里结合真实项目 xiangmu 的
system-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-catch | impl 层集中做(Fail-closed 等),一处维护 |
| 3 | 统一解包响应 | 每个调用方各解 ApiResponse | impl 层解好,返回干净业务类型 |
| 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 | 框架机制:容器创建并注入对象 | SystemClientAutoConfiguration 把 Impl 注册成 Bean,构造注入给消费方 |
一句话:client-sdk 用「依赖倒置」做设计,借「Spring 的 IoC/DI」来落地。 前者是思想,后者是把思想变成现实的工具,两者搭配。
6.3 DIP 的威力:实现可替换
因为消费方只依赖接口,哪天底层把 Feign 换成 gRPC、或换成 Spring 6 的 RestClient,只要改 Impl、接口不变,所有消费方零改动。 这种"实现可替换"正是依赖倒置的核心价值——而常规做法里消费方直接绑死 @FeignClient,就享受不到。
💡 顺带一提:client-sdk 还叠了两个经典设计模式——
api接口层屏蔽 Feign/解包/降级的复杂性,是门面模式 (Facade);impl把ApiResponse<Boolean>适配成干净的boolean,是适配器模式 (Adapter)。但主线就是 DIP。
七、client-sdk 和 common-sdk 的区别
别把两种 SDK 搞混(呼应《公共模块封装》那课):
| xiangmu-common(公共 SDK) | xxx-client-sdk(调用 SDK) | |
|---|---|---|
| 解决什么 | 各服务自己内部要用的通用能力(统一响应、异常、工具、JWT) | 调用别的服务这件事 |
| 谁维护 | 平台/架构组统一维护 | 被调用的那个服务自己维护 |
| 一个项目有几个 | 1 个 | 每个对外提供能力的服务各有 1 个 |
记忆:common-sdk 是"我自己用的工具箱";client-sdk 是"我发给别人的、怎么调用我的说明书 + 现成客户端"。
八、小结
七句话带走:
- client-sdk 由服务提供方封装:谁的服务,谁维护"怎么调我"。
- 标准分层:
api接口层(对外)、impl实现层(降级+解包+日志)、feign原始客户端(内部)、dto共享契约、config自动装配。 - 铁律:消费方只依赖接口层,绝不直接用
@FeignClient。 - 脏活集中做一次:解包、降级、日志、上下文透传都在 SDK 里,所有调用方复用。
- 消费方零样板:依赖 SDK +
@EnableFeignClients+ 注入接口即可,不写 Feign/DTO/降级。 - 本质是依赖倒置原则(DIP):面向接口编程、实现可替换;借 Spring IoC/DI 落地。
- 和 common-sdk 区分:common 是"自己用的工具",client-sdk 是"给别人调我的客户端"。