对服务方法进行分页调用,而不更改原始方法的签名



我有一个用例,我需要在不更改现有签名的情况下将分页输入(即页码和页面大小)添加到现有服务调用(返回结果列表)中(因为它会破坏现有客户端)。实现此目的的一种方法是,我们在 threadlocal 中设置输入,并让实现读取 threadlocal 并执行其分页逻辑。从代码的角度来看,它看起来像这样:

try {
    PaginationKit.setPaginationInput(pageSize, pageNumber); // Set threadlocal
    List<SpecialObject> results = specialService.getSpecialObjs(); //results count will be equal to pageSize value
} finally {
    PaginationKit.clearPaginationInput(); // Clear threadlocal
}

从客户的角度来看,这并不优雅,我想将此功能包装成一些更好的语法糖。我想到了两种方法,我想知道这是否是一个足够通用的用例,已经在其他地方作为模式解决了。有很多这样的服务,试图为每个服务建立一个装饰器是不可取的。

方法1:我喜欢Mockito.when(methodCall).thenReturn(result)糖的嘲笑风格。因此,代码可能如下所示:

SpecialService specialService = PaginationDecorator.prepare(SpecialService.class); // Get a spy that is capable of forwarding calls to the actual instance
List<SpecialObject> results = PaginationDecorator.withPageSize(pageSize).onPage(pageNumber).get(specialService.getSpecialObjs()).get(); // The get() is added to clear the threadlocal

我试图从 Mockito 借用代码来创建间谍,但OngoingStubbing<T>接口在随后的调用链/创建代码中交织在一起,并且闻到了我应该避免的东西。

方法2:使用 java.util.Function 捕获方法调用,并接受两个附加参数 pageNumber 和 pageSize 以与 threadlocals 一起玩。代码可能如下所示

List<SpecialObject> results = PaginationDecorator.withPaging(specialService.getSpecialObjs(), pageSize, pageNumber);

分页装饰器.java:

public static List<T> withPaging(Function<U, List<T>> call, int pageSize, int pageNumber) {
    try {
        PaginationKit.setPaginationInput(pageSize, pageNumber); // Set threadlocal
        return call.apply(); // Clearly, something is missing here!
    } finally {
        PaginationKit.clearPaginationInput(); // Clear threadlocal
    }
}

我无法在这里清楚地制定如何正确使用调用。

有人可以告诉我:

  • 这两者中哪一种是更好的方法
  • 如果这在其他地方使用不同的方法作为配方提供
  • 提出实施方法1或2的前进方向。就个人而言,#2 对我来说似乎更干净(如果它有效)。

请随时批评该方法,并提前感谢您的阅读!

PS:我也喜欢这个问题中的迭代器配方,但仍然需要句法糖的主要问题。

您的第二个变体不起作用,因为您使用了错误的接口(Function需要一个输入参数),并且没有语法来创建函数实例,而只是一个普通的调用表达式。

您有多种选择

  1. 使用Supplier .此接口描述一个没有参数并返回值的函数。

    public static <T> T withPaging(Supplier<T> call, int pageSize, int pageNumber) {
        try {
            PaginationKit.setPaginationInput(pageSize, pageNumber); // Set threadlocal
            return call.get();
        } finally {
            PaginationKit.clearPaginationInput(); // Clear threadlocal
        }
    }
    

    我们没有坚持要求它返回List<T>,而是简单地允许任何提高其多功能性的返回类型。它包括返回某物List的可能性。

    然后,我们可以使用任一方法引用

    List<SpecialObject> results=PaginationDecorator.withPaging(
                        specialService::getSpecialObjs, pageSize, pageNumber);
    

    或 lambda 表达式:

    List<SpecialObject> results=PaginationDecorator.withPaging(
                        () -> specialService.getSpecialObjs(), pageSize, pageNumber);
    
  2. 保持Function,但允许调用方传递所需的参数

    public static <T,R> R withPaging(
        Function<T,R> call, T argument, int pageSize, int pageNumber) {
        try {
            PaginationKit.setPaginationInput(pageSize, pageNumber); // Set threadlocal
            return call.apply(argument);
        } finally {
            PaginationKit.clearPaginationInput(); // Clear threadlocal
        }
    }
    

    现在,调用方必须提供一个函数和一个值。由于预期方法是实例方法,因此可以将接收器实例视为函数参数

    然后,可以再次指定该函数作为(现在未绑定的)方法引用

    List<SpecialObject> results=PaginationDecorator.withPaging(
            SpecialService::getSpecialObjs, specialService, pageSize, pageNumber);
    

    或 lambda 表达式:

    List<SpecialObject> results=PaginationDecorator.withPaging(
            ss -> ss.getSpecialObjs(), specialService, pageSize, pageNumber);
    
  3. 两者都有另一种选择,诉诸AutoCloseable和尝试资源,而不是try…finally。将帮助程序类定义为:

    interface Paging extends AutoCloseable {
        void close();
        static Paging withPaging(int pageSize, int pageNumber) {
            PaginationKit.setPaginationInput(pageSize, pageNumber);
            return ()->PaginationKit.clearPaginationInput();
        }
    }
    

    并像使用它一样使用

    List<SpecialObject> results;
    try(Paging pg=Paging.withPaging(pageSize, pageNumber)) {
        results=specialService.getSpecialObjs();
    }
    

    优点是这不会破坏有关预期操作的代码流,即与 lambda 表达式不同,您可以修改受保护代码中的所有局部变量。如果您忘记将withPaging的结果放在正确的try(…)语句中,最近的 IDE 也会警告您。此外,如果抛出异常并在清理过程中发生另一个异常(即 clearPaginationInput() ),与finally不同,次要异常不会屏蔽主要异常,而是通过addSuppressed记录。

    这就是我在这里更喜欢的。

对于此任务,您的第二种方法似乎更清晰,更容易。至于实现,你可以使用Java的Proxy类。这似乎很容易。我不知道有任何库可以以某种方式使它变得更加容易。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class PagingDecorator {
    public static void setPaginationInput(int pageSize, int pageNumber) {
    }
    public static void clearPaginationInput() {
    }
    public static <T> T wrap(final Class<T> interfaceClass, final T object, final int pageSize, final int pageNumber) {
        if (object == null) {
            throw new IllegalArgumentException("argument shouldn't be null");
        }
        ClassLoader classLoader = object.getClass().getClassLoader();
        return (T) Proxy.newProxyInstance(classLoader, new Class[]{interfaceClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                setPaginationInput(pageSize, pageNumber);
                try {
                    return method.invoke(object, args);
                } catch (InvocationTargetException e) {
                    throw e.getTargetException();
                } catch (Exception e) {
                    throw e;
                } finally {
                    clearPaginationInput();
                }
            }
        });
    }
    public static <T> T wrap(final T object, final int pageSize, final int pageNumber) {
        if (object == null) {
            throw new IllegalArgumentException("argument shouldn't be null");
        }
        Class<?>[] iFaces = object.getClass().getInterfaces();
        //you can use all interfaces, when creating proxy, but it seems cleaner to only mock the concreate interface that you want..
        //unfortunately, you can't just grab T as interface here, becuase of Java's generics' mechanic
        if (iFaces.length != 1) {
            throw new IllegalArgumentException("Object implements more than 1 interface - use wrap() with explicit interface argument.");
        }
        Class<T> iFace = (Class<T>) iFaces[0];
        return wrap(iFace, object, pageSize, pageNumber);
    }
    public interface Some {
    }
    public static void main(String[] args) {
        Some s = new Some() {};
        Some wrapped1 = wrap(Some.class, s, 20, 20);
        Some wrapped2 = wrap(s, 20, 20);
    }
}

你使用的是Java 8,对吧?也许您可以添加一个没有任何实现的默认方法(我知道听起来有点奇怪),如下所示:

// Your current service interface
interface ServiceInterface <T> {
    // Existing method
    List<T> getObjects();
    // New method with pagination atributes
    default List<T> getObjects(int pageSize, int pageNumber) {
        throw new UnsupportedOperationException();
    }
}

新服务(具有分页支持)必须覆盖此方法。在我看来,这样你"保持简单"。

最新更新