我有一个用例,我需要在不更改现有签名的情况下向现有服务调用(返回结果列表)添加分页输入(即页码和页面大小)(因为它会破坏现有客户端)。实现这一目标的一种方法是我们在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);
PaginationDecorator.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
}
}
我无法在此明确制定如何正确使用呼叫。
有人可以告诉我:
随意批评这种方法并提前感谢阅读!
P.S。:我也很喜欢iterator recipe in this question,但仍然需要语法糖的主要问题。
答案 0 :(得分:2)
您的第二个变体不起作用,因为您使用了错误的接口(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-with-resource,而不是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
进行记录。
这就是我喜欢的。
答案 1 :(得分:1)
您的第二种方法似乎更简洁,更轻松。至于实现,您可以使用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);
}
}
答案 2 :(得分:0)
您正在使用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();
}
}
新服务(使用分页支持)必须覆盖此方法。在我看来,这样你就可以保持简单&#34;。