Повторное использование JAX RS 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;
}

Но иногда (не всегда) я получаю исключение:

Недопустимое использование BasicClientConnManager: соединение все еще выделено. Обязательно отпустите соединение, прежде чем выделять другой.

Как можно корректно использовать/кэшировать Client/WebTarget? Возможно ли это с JAX RS Client API? Или я должен использовать некоторые функции, связанные с каркасом (resteasy/jersey). Не могли бы вы привести пример или документацию?

Ответ 1

Ваша реализация не является потокобезопасной. Когда два потока обращаются к someMethod, в то же время они используют один и тот же Client, и один попытается сделать второй запрос, пока первый не будет завершен.

У вас есть два варианта:

  • Синхронизировать доступ к Client и WebTarget вручную.
  • Позвольте контейнеру управлять concurrency, аннотируя охватывающий тип @javax.ejb.Singleton, который гарантирует безопасность потока. (см. главу 4.8.5 спецификации

Ответ 2

Так как эта проблема все еще открыта на момент написания (версия 3.0.X) RESTEASY: устаревшая очистка классов Apache

Вы можете глубже использовать новые, не устаревшие классы, чтобы создать для вас клиента 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.

Ответ 3

Во-первых, не используйте повторно 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).

Когда вы устанавливаете connectionPoolSize, вы больше не будете получать ошибку при повторном использовании Клиента, и вы можете с радостью повторно использовать их во всем приложении. Я пробовал это решение во многих проектах, и он действительно работает хорошо. Но когда вы развертываете приложение в контейнер non-resteasy (например, Glassfish), ваш код не будет работать, и вам придется снова использовать класс ClientBuilder.