
本文探讨了如何通过java函数式接口和泛型,优雅地解决feign api分页调用中参数多样性导致的重复代码问题。通过引入统一的`pagingapi`接口和静态工厂方法,我们能够以描述性的方式绑定不同数量的参数,从而实现对各类分页api的通用化处理和数据抽取,显著减少了样板代码,并提升了代码的可维护性和可读性。
在微服务架构中,Feign作为声明式HTTP客户端,极大地简化了服务间调用。然而,当需要处理大量支持分页的Feign API时,如果这些API的入参除了分页信息(页码和大小)外还包含不同数量的其他业务参数,就可能导致大量的样板代码。例如,一个API可能只带一个业务参数,而另一个可能带两个,这使得为每个API编写通用的分页数据抽取逻辑变得复杂且冗余。
问题描述与传统方法的局限
假设我们有一个通用的分页数据抽取服务,其核心逻辑是根据给定的分页API接口,从第一页开始逐页获取数据直到所有数据被抽取完毕。最初的实现可能类似于以下结构:
// 核心抽取逻辑(简化版) publicList > drainFeignPageableCall(PagedCall feignCall) { // ... 调用 feignCall.call(0, 10) 获取第一页 // ... 递归或循环调用获取后续页面 return null; // 实际返回所有页面的数据 } // 抽象分页API调用的接口 public interface PagedCall { BaseFeignResult call(int p, int s); } // 针对单参数API的实现 public static class SingleParamPageableCall implements PagedCall { SingleParamPagingApi fun; String param; public SingleParamPageableCall(SingleParamPagingApi fun, String param) { this.fun = fun; this.param = param; } @Override public BaseFeignResult call(int p, int s) { // 包装实际的Feign调用 return BaseFeignResult. builder() .resp(fun.callFeignApi(param, p, s)) .build(); } } // 针对单参数的Feign接口定义 public interface SingleParamPagingApi { ResponseEntity > callFeignApi(String arg, int page, int size) throws RuntimeException; } // 示例调用 drainFeignPageableCall(new BaseService.SingleParamPageableCall (ordersFeignClient::getOrdersBySampleIds, "34596"));
这种方法的问题在于,每当遇到一个具有不同数量业务参数的分页API时(例如,两个参数、三个参数),我们就需要为它定义一个新的接口(如TwoParamPagingApi)和一个新的PagedCall实现类(如TwoParamPageableCall),这导致了大量的样板代码和类型膨胀,难以维护。我们期望的是一种更具描述性、更函数式的实现方式,能够将参数映射到方法调用的过程抽象化,而无需定义繁重的中间对象。
使用函数式接口实现参数绑定与抽象
Java 8引入的函数式接口为解决这类问题提供了强大的工具。我们可以通过定义不同参数数量的函数式接口,并在一个统一的接口中提供静态工厂方法来绑定这些参数,从而将复杂的API签名转换为一个只接受分页参数的简单接口。
立即学习“Java免费学习笔记(深入)”;
核心接口定义
首先,我们定义针对不同参数数量的原始Feign API调用的函数式接口。这里以一个参数和两个参数为例:
// 针对一个业务参数的Feign API接口 public interface PagingApi1{ ResponseEntity > callFeignApi(A0 arg0, int page, int size) throws RuntimeException; } // 针对两个业务参数的Feign API接口 public interface PagingApi2 { ResponseEntity > callFeignApi(A0 arg0, A1 arg1, int page, int size) throws RuntimeException; }
接下来,定义一个统一的PagingApi接口,它只关心分页参数(页码和大小),并提供静态工厂方法来“适配”上述不同参数数量的原始API。
// 统一的分页API接口,只接受页码和大小 public interface PagingApi{ // 静态工厂方法:绑定一个业务参数 static PagingApi of(PagingApi1 api, A0 arg0) { return (p, s) -> api.callFeignApi(arg0, p, s); } // 静态工厂方法:绑定两个业务参数 static PagingApi of(PagingApi2 api, A0 arg0, A1 arg1) { return (p, s) -> api.callFeignApi(arg0, arg1, p, s); } // 实际执行Feign API调用的方法 ResponseEntity > callFeignApi(int page, int size) throws RuntimeException; }
通过PagingApi.of()方法,我们可以将一个具有多个参数的Feign方法引用(如ordersFeignClient::getOrdersBySampleIds)和其固定的业务参数绑定起来,生成一个只接受页码和大小的PagingApi实例。这实现了参数的“柯里化”或部分应用。
通用PageableCall实现
有了统一的PagingApi,我们的PagedCall接口的实现也变得极其简洁:
pui 是一款基于jQyery开发的插件库。目前线上稳定使用已有2年多,丰富的接口,简单明了的调用方式,灵活的回调函数,让您轻轻松松打造出富有灵活交互的Web前端界面解决方案。 插件库封装了布局、表单元素、表单校验、弹窗、toast、气泡pop、tab切换、日历时间、分页、表格、树、css命名等功能
// 通用的 PageableCall 实现 public static class PageableCallimplements PagedCall { PagingApi fun; // 现在它只依赖于统一的 PagingApi public PageableCall(PagingApi fun) { this.fun = fun; } @Override public BaseFeignResult call(int p, int s) { BaseFeignResult.BaseFeignResultBuilder builder = BaseFeignResult.builder(); try { builder.resp(fun.callFeignApi(p, s)); // 直接调用统一接口 } catch (RuntimeException e) { builder.excp(e); } return builder.build(); } }
这里的BaseFeignResult和IVDPagedResponseOf是根据原始问题上下文假设的数据结构,用于封装Feign调用的响应和异常。
实际调用示例
现在,调用通用的分页数据抽取服务变得非常简洁和描述性:
// 假设 ordersFeignClient.getOrdersBySampleIds 方法签名是: // ResponseEntity> getOrdersBySampleIds(String sampleId, int page, int size); drainFeignPageableCall( new PageableCall ( PagingApi.of(ordersFeignClient::getOrdersBySampleIds, "34596") ) );
通过PagingApi.of(ordersFeignClient::getOrdersBySampleIds, "34596"),我们以函数式的方式描述了如何将"34596"这个参数绑定到getOrdersBySampleIds方法上,从而生成了一个新的PagingApi实例,这个实例在内部已经“记住”了"34596"这个参数,后续调用时只需提供页码和大小即可。
进一步优化与注意事项
-
接口合并: PagingApi和PagedCall在功能上非常相似,都可以进一步合并为一个接口,例如直接让PagingApi实现PagedCall的功能,或者将PagingApi作为PagedCall的唯一接口。
// 合并 PagingApi 和 PagedCall public interface PagingApi
{ // 静态工厂方法不变 static PagingApi of(PagingApi1 api, A0 arg0) { return (p, s) -> api.callFeignApi(arg0, p, s); } // ... 其他 of 方法 // 原始的 callFeignApi 方法,现在可以作为 PagedCall 的实现 ResponseEntity > callFeignApi(int page, int size) throws RuntimeException; // 包装成 BaseFeignResult 的方法,可以直接在 PagingApi 内部实现 default BaseFeignResult call(int p, int s) { BaseFeignResult.BaseFeignResultBuilder builder = BaseFeignResult.builder(); try { builder.resp(callFeignApi(p, s)); } catch (RuntimeException e) { builder.excp(e); } return builder.build(); } } // 此时 drainFeignPageableCall 的签名可以变为: // public List > drainFeignPageableCall(PagingApi feignCall) { ... } // 调用方式简化为: // drainFeignPageableCall(PagingApi.of(ordersFeignClient::getOrdersBySampleIds, "34596")); -
避免递归调用: 在drainFeignPageableCall方法中,原始的递归实现虽然具有函数式风格,但在Java中可能导致栈溢出(StackOverflowError),尤其是在处理大量页面时。更健壮的做法是使用迭代(for或while循环)来替代递归。
// 迭代实现的 drainFeignPageableCall 示例 public
List > drainFeignPageableCall(PagingApi feignCall, int pageSize) { List > allResults = new ArrayList<>(); int page = 0; boolean hasMore = true; while (hasMore) { BaseFeignResult currentPageResult = feignCall.call(page, pageSize); allResults.add(currentPageResult); // 假设 IVDPagedResponseOf 有一个 getTotalPages 或 getContent() 方法 // 这里需要根据实际的 IVDPagedResponseOf 结构来判断是否还有下一页 // 简单示例:如果当前页返回的数据量小于 pageSize,则认为没有更多数据了 // 更准确的判断应基于 totalElements 或 totalPages if (currentPageResult.resp != null && currentPageResult.resp.getBody() != null) { List data = currentPageResult.resp.getBody().getData(); if (data == null || data.size() < pageSize) { hasMore = false; } } else { // 处理异常或空响应情况 hasMore = false; } page++; } return allResults; } 请注意,上述迭代逻辑中的currentPageResult.resp.getBody().getData().size()
总结
通过引入Java的函数式接口和静态工厂方法,我们能够以高度抽象和描述性的方式处理具有不同参数签名的分页Feign API。这种方法避免了为每种参数组合创建大量中间接口和实现类,显著减少了样板代码,提高了代码的可读性和可维护性。同时,结合迭代而非递归的抽取逻辑,可以构建一个既优雅又健壮的通用分页数据抽取服务。









