我有一个用例,我需要在不更改现有签名的情况下将分页输入(即页码和页面大小)添加到现有服务调用(返回结果列表)中(因为它会破坏现有客户端)。实现此目的的一种方法是,我们在 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
需要一个输入参数),并且没有语法来创建函数实例,而只是一个普通的调用表达式。
您有多种选择
-
使用
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);
-
保持
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);
-
两者都有另一种选择,诉诸
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();
}
}
新服务(具有分页支持)必须覆盖此方法。在我看来,这样你"保持简单"。