在多线程环境中重用 JAX RS 客户端(使用 resteasy)

Reusing JAX RS Client in multi-threaded environment (with resteasy)

根据文档,

"Clients are heavy-weight objects that manage the client-side communication infrastructure. Initialization as well as disposal of a Client instance may be a rather expensive operation. It is therefore advised to construct only a small number of Client instances in the application. "

好的,我正在尝试将 Client 本身和 WebTarget 实例缓存在静态变量中,someMethod() 在多线程环境中被调用:

private static Client client = ClientBuilder.newClient();
private static WebTarget webTarget = client.target("someBaseUrl");
...
public static String someMethod(String arg1, String arg2)
{
    WebTarget target = entrTarget.queryParam("arg1", arg1).queryParam("arg2", arg2);
    Response response = target.request().get();
    final String result = response.readEntity(String.class);
    response.close();
    return result;
}

但有时(不总是)我遇到异常:

Invalid use of BasicClientConnManager: connection still allocated. Make sure to release the connection before allocating another one.

Client/WebTarget如何才能正确地reused/cached? JAX RS 客户端 API 是否可行?或者我必须使用一些特定于框架的功能 (resteasy/jersey) 你能提供一些示例或文档吗?

您的实现不是线程安全的。当两个线程同时访问 someMethod 时,它们共享相同的 Client 并且一个线程将尝试在第一个请求未完成时发出第二个请求。

你有两个选择:

  • 手动同步对 ClientWebTarget 的访问。
  • 通过使用 @javax.ejb.Singleton 注释封闭类型来让容器管理并发性,从而保证线程安全。 (参见 EJB specification 的第 4.8.5 章)

如果 someMethod 在容器管理的环境中,我会使用第二种方法。

首先,不要重复使用 WebTarget。为简单起见,您始终可以创建新的 WebTarget。

其次,如果您使用的是 Resteasy,您可以将提供的 Resteasy 客户端依赖项添加到您的项目中。 Gradle 中的示例:

    provided 'org.jboss.resteasy:resteasy-client:3.0.14.Final'

然后,您可以像这样创建连接:

        ResteasyClientBuilder builder = new ResteasyClientBuilder();
        builder.connectionPoolSize(200);

无需设置 maxPooledPerRoute,这是由 RestEasy 自动设置的(可在 RestEasyClientBuilder class 源代码中找到)。

设置 connectionPoolSize 后,在重用 Client 时将不会再出现错误,您可以愉快地在整个应用程序中重用它们。我已经在很多项目上尝试过这个解决方案,而且效果很好。但是,当您将应用程序部署到非 Resteasy 容器(如 Glassfish)时,您的代码将无法运行,您将不得不再次使用 ClientBuilder class。

由于在撰写本文时此问题仍未解决(版本 3.0.X)RESTEASY: deprecated Apache classes cleanup

您可以更深入地使用更新的、未弃用的 类 来创建您的 resteasy 客户端。您还可以更好地控制您希望游泳池的样子等。

这是我所做的:

// This will create a threadsafe JAX-RS client using pooled connections.
// Per default this implementation will create no more than than 2
// concurrent connections per given route and no more 20 connections in
// total. (see javadoc of PoolingHttpClientConnectionManager)
PoolingHttpClientConnectionManager cm =
        new PoolingHttpClientConnectionManager();

CloseableHttpClient closeableHttpClient =
        HttpClientBuilder.create().setConnectionManager(cm).build();
ApacheHttpClient4Engine engine =
        new ApacheHttpClient4Engine(closeableHttpClient);
return new ResteasyClientBuilder().httpEngine(engine).build();

另外确保在拨打电话后释放连接。调用 response.close() 将为您完成此操作,因此可能会将其放在 finally 块中。

不幸的是,文档对于什么可以和不能安全地重复使用不是很清楚。如有疑问,请不要重复使用。但是,如果您决心最大限度地减少开销,则可以根据调用的方法安全地重用大多数对象。

从您的代码开始,这里有一些关于正在发生的事情的评论:

// (1) Store an instance of Client with its own configuration
private static Client client = ClientBuilder.newClient();
// (2) Store an instance of WebTarget with its own configuration (inherited from client)
private static WebTarget webTarget = client.target("someBaseUrl");
...
public static String someMethod(String arg1, String arg2)
{
    // (3) New instance of WebTarget (copy entrTarget config) with "arg1" param
    // (4) New instance of WebTarget (copy anonymous config) with "arg2" param
    WebTarget target = entrTarget.queryParam("arg1", arg1).queryParam("arg2", arg2);
    // (5) New instance of Invocation.Builder (copy target config)
    // (6) Invoke GET request with stored configuration
    Response response = target.request().get();
    final String result = response.readEntity(String.class);
    response.close();
    return result;
}

我评论了代码 as-is,但我猜 (3) 应该引用了静态 webTarget 字段。

这里正在创建很多对象。每次创建一个对象时,都会有一个新实例和它自己的配置副本(因此它不会影响它的前辈)。在这种特殊情况下,应该没有竞争条件,但肯定有可能出错的方法。

如果您在 (3) 之前做过这样的事情(假设这些是合法财产):

WebTarget target = webTarget.property("foo", fooProperty).queryParam("arg1", arg1);

然后您将更改静态 webTarget 字段的配置,这可能会导致竞争条件。有很多方法可以从静态字段更改配置,因此您要么需要小心保护它们,要么根本不使用它们。

此外,请注意,几乎每个从原始 client 生成的对象都会引用它,以确定 httpEngine 是否已关闭。因此,除非您尝试正常关闭您的应用程序,否则关闭客户端可能永远不是一个好主意。

我通过挖掘源代码发现了所有这些,因为真的没有好的参考。