我们有一组Java应用程序,它们最初是使用常规同步方法编写的,但在很大程度上已转换为异步Vert.x(常规API,而不是Rx)。我们在同步和异步代码之间的界限上遇到了一些麻烦,特别是当我们有一个必须同步的方法(下面的推理解释)并且我们想要从中调用异步方法时。
之前在Stack Overflow上提出了许多类似的问题,但实际上所有这些问题都在C#环境中,并且答案似乎不适用。
除此之外,我们还在使用Geotools和Apache Shiro。两者都使用他们定义的严格同步的API通过扩展来提供定制。作为一个具体的例子,我们的Shiro自定义授权领域需要访问我们的用户数据存储,为此我们创建了一个异步DAO API。我们必须编写的Shiro方法称为doGetAuthorizationInfo;
,预计会返回AuthorizationInfo
。但似乎没有一种可靠的方法可以从异步DAO API的另一端访问授权数据。
在线程不是由Vert.x创建的特定情况下,使用CompletableFuture
是一个可行的解决方案:同步doGetAuthorizationInfo
会将异步工作推送到Vert.x线程,然后阻止CompletableFuture.get()
中的当前线程,直到结果变为可用。
不幸的是,可以在Vert.x线程上调用Shiro(或Geotools或其他)方法。在这种情况下,阻止当前线程是非常糟糕的:如果它是事件循环线程,那么我们打破黄金法则,而如果它是工作线程(例如,通过Vertx.executeBlocking
),那么阻止它将阻止工作者接收来自队列的更多内容 - 意味着阻止将是永久性的。
这个问题有“标准”解决方案吗?在我看来,只要在可扩展的同步库下使用Vert.x,它就会出现。这只是人们避免的情况吗?
......更详细一点。这是来自org.apache.shiro.realm.AuthorizingRealm的片段:
/**
* Retrieves the AuthorizationInfo for the given principals from the underlying data store. When returning
* an instance from this method, you might want to consider using an instance of
* {@link org.apache.shiro.authz.SimpleAuthorizationInfo SimpleAuthorizationInfo}, as it is suitable in most cases.
*
* @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
* @return the AuthorizationInfo associated with this principals.
* @see org.apache.shiro.authz.SimpleAuthorizationInfo
*/
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
我们的数据访问层有如下方法:
void loadUserAccount(String id, Handler<AsyncResult<UserAccount>> handler);
我们如何从前者中援引后者?如果我们知道在非Vert.x线程中调用doGetAuthorizationInfo
,那么我们可以这样做:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
CompletableFuture<UserAccount> completable = new CompletableFuture<>();
vertx.<UserAccount>executeBlocking(vertxFuture -> {
loadUserAccount((String) principals.getPrimaryPrincipal(), vertxFuture);
}, res -> {
if (res.failed()) {
completable.completeExceptionally(res.cause());
} else {
completable.complete(res.result());
}
});
// Block until the Vert.x worker thread provides its result.
UserAccount ua = completable.get();
// Build up authorization info from the user account
return new SimpleAuthorizationInfo(/* etc...*/);
}
但是如果在Vert.x线程中调用doGetAuthorizationInfo
,那么事情就完全不同了。上面的技巧将阻止一个事件循环线程,所以这是一个禁忌。或者,如果它是一个工作线程,那么executeBlocking
调用将把loadUserAccount
任务放到同一个工作人员的队列中(我相信),因此随后的completable.get()
将永久阻止。
我打赌你已经知道了答案,但是希望不是这样 - 如果对GeoTools或Shiro的调用需要阻止等待某些事情的响应,那么你不应该在Vert.x上进行调用线。
您应该创建一个带有线程池的ExecutorService
,您应该使用它来执行这些调用,安排每个提交的任务在完成后发送Vert.x消息。
您可以在移动到线程池中的块大小方面具有一定的灵活性。您可以在调用堆栈的上方移动更大的内容,而不是紧紧包装这些调用。您可能会根据需要更改的代码来做出此决定。由于使方法异步通常意味着改变其调用堆栈中的所有同步方法(这是这种异步模型的不幸基本问题),您可能希望在堆栈上做得很高。
您最终可能会得到一个适配器层,为各种同步服务提供Vert.x API。