Java泛型详解 —— 出参入参绑定技巧
1. 泛型出参入参绑定
公司 p8 大佬给我出了一道题:我希望执行 execute() 方法之后,不同的调用方能够返回不同的响应结果,并且 execute()方法内部是没有任何实现的。例如:RequestA 调用方法后会返回 ResponseA,RequestB 调用方法后会返回 ResponseB
那具体应该如何实现呢?我们首先想到的肯定是需要通过泛型来建立 Request 和 Response 之间的联系,那这个泛型类型应该如何写呢?
代码:
- 定义 execute 泛型方法
public class Actuator {// 泛型方法public static <T> T execute(Request<T> req) {return null;}
}
我们定义了一个泛型方法 execute,这个方法巧妙的地方在于形参接收一个 Request,返回值是 Request 的泛型参数
🤔 思考:这里不用泛型方法可以吗?
那肯定是不可以的,原因有两点:
- Actuator 不是泛型类,这种使用场景下肯定是不希望把 Actuator 声明成泛型类的,不然执行一次 execute 方法就要 new 一个对应类型的 Actuator 实例
- execute 方法是一个静态方法,就算 Actuator 是泛型类,静态方法也是不可以使用泛型类中的泛型参数的
- 定义 Request 抽象类
/*** @Description TODO* @Author Mr.Zhang* @Date 2025/4/24 22:10* @Version 1.0*/
public abstract class Request<T> {}
因为将来不同的 Request 实现类都会调用同一个 execute 方法,所以为了代码的复用性,抽取一个 Request 抽象类来当作 execute 方法的入参
- 定义 RequestA、RequestB 实现类
/*** @Author: ZhangGongMing* @CreateTime: 2025/4/25 09:15* @Description:* @Version: 1.0*/
public class RequestA extends Request<ResponseA> {
}/*** @Author: ZhangGongMing* @CreateTime: 2025/4/25 09:16* @Description:* @Version: 1.0*/
public class RequestB extends Request<ResponseB> {
}
不同的实现类指定不同的泛型参数
- 定义 ResponseA、ResponseB
@Data
public class ResponseA {
}@Data
public class ResponseB {
}
- 测试是否成功
public static void main(String[] args) {RequestA requestA = new RequestA();RequestB requestB = new RequestB();ResponseA responseA = execute(requestA);ResponseB responseB = execute(requestB);
}
我们发现,泛型成功转换了!
那这个知识点学了有什么用呢?一开始博主也对其使用场景百思不得其解。直到在一次偶然的机会看到了拼多多和京东的对外 SDK 包源码,我发现了这种用法的使用场景:出参入参绑定配合统一入口方法实现统一入口规范
2. 实际使用案例
最近在阅读拼多多和京东的 SDK 源码的时候,发现它们都不约而同的使用到了 泛型出参入参绑定 结合 统一入口方法 的方式来实现客户端 client 和服务端
我们发现他们在定义方法入参的时候都指定了对应接口的出参。我在深入分析后,才发现了这样设计的独到之处
2.1. 使用场景
在介绍代码实现之前,先说一下出参入参绑定这种方式的使用场景:
- 把需要对外发布的接口封装成一个 SDK,给外部系统调用(主要使用场景)
- 调用外部系统接口,但是外部系统没有提供 SDK,只提供了入参和出参的定义
注:出参入参绑定这种方式只有这两种使用场景!
2.2. 实际案例
我们以需求带入场景,通过代码带大家深入体会在这两种使用场景下使用出参入参绑定的效果
需求如下:
假设现在有两个接口,分别是商品详情接口和商品列表接口
- 如果以上两个接口是我们内部需要提供的对外接口,我们应该如何实现对外提供服务的 SDK?
- 如果以上两个接口是外部系统提供好的,我们需要调用应该如何实现?
代码实现
以上两种使用场景在代码实现上完全一致,此处以我们平常接触最多的调用第三方接口的场景为例
如果是我们调用外部系统提供好的接口,并且外部系统只提供了入参和出参的定义,需要我们自己实现调用方法
a. 常规实现
- 首先我们需要定义商品详情接口和商品列表接口的请求参数和响应参数
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/16 23:44* @Description: 商品详情请求类* @Version: 1.0*/
@Data
public class GoodsDetailRequest {/*** 商品 ID*/private String goodsId;
}/*** @Author: ZhangGongMing* @CreateTime: 2025/5/16 23:45* @Description: 商品详情响应类* @Version: 1.0*/
@Data
public class GoodsDetailResponse implements Serializable {@Serialprivate static final long serialVersionUID = 1L;/*** 商品 ID*/private String goodsId;/*** 商品编码*/private String goodsCode;/*** 商品价格*/private String goodsPrice;/*** 商品名称*/private String goodsName;/*** 商品描述*/private String goodsDesc;/*** 商品图片*/private String goodsImg;
}
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/16 23:54* @Description:* @Version: 1.0*/
@Data
public class GoodsListRequest implements Serializable {@Serialprivate static final long serialVersionUID = 1L;/*** 商品 ID 列表*/private List<String> goodsIds;/*** 商品名称*/private String goodsName;}/*** @Author: ZhangGongMing* @CreateTime: 2025/5/16 23:58* @Description:* @Version: 1.0*/
@Data
public class GoodsListResponse implements Serializable {@Serialprivate static final long serialVersionUID = 1L;/*** 商品列表*/private List<GoodsItem> goodsList;class GoodsItem implements Serializable {/*** 商品 ID*/private String goodsId;/*** 商品编码*/private String goodsCode;/*** 商品价格*/private String goodsPrice;/*** 商品名称*/private String goodsName;/*** 商品描述*/private String goodsDesc;/*** 商品图片*/private String goodsImg;}
}
- 定义 Client,接收不同接口响应数据
@Service
@AllArgsConstructor
public class Client {/*** 此处这个方法的作用是外部系统开放给我们的请求接口.通过传参到这个接口拿到返回回来的响应* <p>* TODO 需要注意的是: 通过 URL 调用到外部系统时,由于涉及到网络传输,需要将返回结果序列化为 JSON 字符串返回调用方** @param body* @return*/private String postRequest(String body) {return HttpUtil.createPost("http://localhost:8080/api").body(body).execute().body();}/*** 获取商品列表** @param request* @return*/public GoodsListResponse getGoodsList(GoodsListRequest request) {String result = postRequest(JSONUtil.toJsonStr(request));return JSONUtil.toBean(result, GoodsListResponse.class);}/*** 获取商品详情** @param request* @return*/public GoodsDetailResponse getGoodsDetail(GoodsDetailRequest request) {String result = postRequest(JSONUtil.toJsonStr(request));return JSONUtil.toBean(result, GoodsDetailResponse.class);}
}
- 测试
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/17 00:27* @Description:* @Version: 1.0*/
public class Main {/*** 常规实现 ** @param args*/public static void main(String[] args) {Client client = new Client();GoodsListRequest goodsListRequest = new GoodsListRequest();GoodsListResponse goodsListResponse = client.getGoodsList(goodsListRequest);System.out.println(goodsListResponse);GoodsDetailRequest goodsDetailRequest = new GoodsDetailRequest();GoodsDetailResponse goodsDetailResponse = client.getGoodsDetail(goodsDetailRequest);System.out.println(goodsDetailResponse);}
}
这种实现方式看起来并没有什么问题,也是我们平常最常使用的,但仔细想想这种实现方式有什么弊端呢?
- 不够灵活:当前只有两个接口还好。但假设需要调用外部系统的一百个接口,那岂不是意味着我们需要在 Client 类中写一百个不同的方法来对应不同的接口,且这些方法都是千篇一律的调用同一个
postRequest()
方法,以及将响应数据解析成对应的返回结果对象返回。这样的实现方式显然不好,大致相同的实现逻辑却要一直重复写。太不优雅了!
b. 出参入参绑定实现
那为了优雅且方便的实现对接外部系统的众多接口,我们不妨学习拼多多和京东的做法,使用出参入参绑定的方式来优雅实现
- 定义基类请求类,通过泛型实现出参入参绑定
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/17 18:08* @Description: 请求基类* @Version: 1.0*/
public abstract class BaseRequest<T extends BaseResponse> {/*** 请求方法名TODO methodName 参数的作用是通过统一方法调用的时候,告诉服务提供方自己要调用的是哪个方法*/private String methodName;public abstract Class<T> getResponseType();
}
- 定义基类响应类
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/17 18:08* @Description: 响应基类* @Version: 1.0*/
@Data
public abstract class BaseResponse implements Serializable {@Serialprivate static final long serialVersionUID = 1L;/*** 响应码*/private String code;/*** 响应信息*/private String message;
}
- 定义商品详情接口和商品列表接口的请求类和响应类,分别实现基类请求类和基类响应类
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/17 18:14* @Description:* @Version: 1.0*/
@Data
public class GoodsDetailRequest extends BaseRequest<GoodsDetailResponse> {/*** 商品 ID*/private String goodsId;@Overridepublic Class<GoodsDetailResponse> getResponseType() {return GoodsDetailResponse.class;}
}/*** @Author: ZhangGongMing* @CreateTime: 2025/5/17 18:14* @Description:* @Version: 1.0*/
@Data
public class GoodsDetailResponse extends BaseResponse {/*** 商品 ID*/private String goodsId;/*** 商品编码*/private String goodsCode;/*** 商品价格*/private String goodsPrice;/*** 商品名称*/private String goodsName;/*** 商品描述*/private String goodsDesc;/*** 商品图片*/private String goodsImg;
}/*** @Author: ZhangGongMing* @CreateTime: 2025/5/17 18:17* @Description:* @Version: 1.0*/
@Data
public class GoodsListRequest extends BaseRequest<GoodsListResponse> {/*** 商品 ID 列表*/private List<String> goodsIds;/*** 商品名称*/private String goodsName;@Overridepublic Class<GoodsListResponse> getResponseType() {return GoodsListResponse.class;}
}/*** @Author: ZhangGongMing* @CreateTime: 2025/5/16 23:58* @Description:* @Version: 1.0*/
@Data
public class GoodsListResponse extends BaseResponse {/*** 商品列表*/private List<GoodsItem> goodsList;class GoodsItem implements Serializable {/*** 商品 ID*/private String goodsId;/*** 商品编码*/private String goodsCode;/*** 商品价格*/private String goodsPrice;/*** 商品名称*/private String goodsName;/*** 商品描述*/private String goodsDesc;/*** 商品图片*/private String goodsImg;}
}
- 定义 client 类,通过 execute() 方法接收不同接口的响应参数
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/17 18:19* @Description:* @Version: 1.0*/
@Service
@AllArgsConstructor
public class Client {/*** 此处这个方法的作用是外部系统开放给我们的请求接口.通过传参到这个接口拿到返回回来的响应* <p>* TODO 需要注意的是: 通过 URL 调用到外部系统时,由于涉及到网络传输,需要将返回结果序列化为 JSON 字符串返回调用方** @param body* @return*/private String postRequest(String body) {return HttpUtil.createPost("http://localhost:8080/api").body(body).execute().body();}/*** 统一执行方法,外部系统开放给我们的所有 API 接口,都可以通过这一个方法实现调用.提高了代码优雅以及降低了冗余性** @param request* @return* @param <T>*/public <T extends BaseResponse> T execute(BaseRequest<T> request) {// TODO 从服务端接收到的结果会序列化为 JSON 字符串String result = postRequest(JSONUtil.toJsonStr(request));return JSONUtil.toBean(result, request.getResponseType());}
}
- 测试
/*** @Author: ZhangGongMing* @CreateTime: 2025/5/18 21:34* @Description:* @Version: 1.0*/
public class Main {public static void main(String[] args) {Client client = new Client();GoodsDetailRequest goodsDetailRequest = new GoodsDetailRequest();GoodsDetailResponse goodsDetailResponse = client.execute(goodsDetailRequest);System.out.println(goodsDetailResponse.toString());GoodsListRequest goodsListRequest = new GoodsListRequest();GoodsListResponse goodsListResponse = client.execute(goodsListRequest);System.out.println(goodsListResponse.toString());}
}
通过对比常规实现中,我们使用出参入参绑定的这种方式的好处是显而易见的
- 无论外部系统给我们开放多少个 API 接口,我们都只需要通过一个
execute()
方法即可。只需要在定义请求类的时候把对应的响应类绑定下即可。这种实现既方便维护,提高代码质量又极其优雅! - 后期如果需要新增对外接口,这个客户端类的代码是完全不用动的,不需要改动原来的业务逻辑,只需要再定义新的接口的入参和出参就可以!这样也符合 Java 的开闭原则:即对扩展开放,对修改关闭
2.3. 深入点
不知道大家有没有疑惑,我明明在前面有提到 统一入口方法 ,为什么一直没有体现呢?
- 大家需要注意的是,无论是哪种使用场景,无论是我们自己封装的 SDK 还是调用外部系统的 API 接口。我们都相当于客户端。而出参入参绑定正是用于客户端的优化技术
- 而统一入口方法实际上是用于服务端的优化手段。通过统一入口方法可以让调用方在只知道一个 URL 路径的前提下,配合方法参数就可以实现自动调用不同的视线方法
-
- 不知道大家有没有注意到,在我的 BaseRequest 基类中有一个 methodName 属性。这个属性就是用来标记不同的视线方法的。例如需要查询商品详情的时候,methodName 就可以赋值为 getGoodsDetail。这样通过服务端那边的技术实现就可以找到对应的实现方法
此处为语雀内容卡片,点击链接查看:统一入口方法 · 语雀
有想要具体了解服务端的优化技术:统一入口方法。可以看我这篇博客~
最后送一句阿里 P8 大佬的话给大家:
RPC框架的本质就是:屏蔽调用目标的差异,屏蔽中间步骤序列化和反序列化差异