使用Java函数式接口抽象分页Feign API调用

本文探讨了如何通过java函数式接口和泛型,优雅地解决feign api分页调用中参数多样性导致的重复代码问题。通过引入统一的`pagingapi`接口和静态工厂方法,我们能够以描述性的方式绑定不同数量的参数,从而实现对各类分页api的通用化处理和数据抽取,显著减少了样板代码,并提升了代码的可维护性和可读性。

在微服务架构中,Feign作为声明式HTTP客户端,极大地简化了服务间调用。然而,当需要处理大量支持分页的Feign API时,如果这些API的入参除了分页信息(页码和大小)外还包含不同数量的其他业务参数,就可能导致大量的样板代码。例如,一个API可能只带一个业务参数,而另一个可能带两个,这使得为每个API编写通用的分页数据抽取逻辑变得复杂且冗余。

问题描述与传统方法的局限

假设我们有一个通用的分页数据抽取服务,其核心逻辑是根据给定的分页API接口,从第一页开始逐页获取数据直到所有数据被抽取完毕。最初的实现可能类似于以下结构:

// 核心抽取逻辑(简化版)
public  List> 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签名转换为一个只接受分页参数的简单接口。

核心接口定义

首先,我们定义针对不同参数数量的原始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接口的实现也变得极其简洁:

// 通用的 PageableCall 实现
public static class PageableCall implements 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"这个参数,后续调用时只需提供页码和大小即可。

进一步优化与注意事项

  1. 接口合并: 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"));
  2. 避免递归调用: 在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。这种方法避免了为每种参数组合创建大量中间接口和实现类,显著减少了样板代码,提高了代码的可读性和可维护性。同时,结合迭代而非递归的抽取逻辑,可以构建一个既优雅又健壮的通用分页数据抽取服务。