REST-клієнти в Java-застосунках

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Всім привіт. Я Сергій Моренець, розробник, викладач, спікер та технічний письменник, хочу в цій серії статей поділитися з вами досвідом роботи з такою актуальною темою, як REST-клієнти. Я вже писав про REST API, як реалізовувати server-side pagination, як обробляти помилки, які конвенції є в REST API, а також про використання DTO. Але це все стосувалося серверної частини. Тут же я хочу, по-перше, узагальнити та систематизувати всю інформацію станом на 2023 рік, по-друге, порівняти всі найбільш популярні технології в сегменті і розповісти про останні тенденції в цій сфері. Сподіваюся, що ця стаття буде корисною для всіх, хто хоче більше дізнатися про те, як написати клієнтську частину для REST API.

Vanilla Java

Що таке клієнт REST і чим він відрізняється від HTTP-клієнта? Я не повторюватиму тут принципи REST API та побудови RESTful вебсервісів, вони всім добре відомі. REST клієнт повинен дозволяти (і допомагати) розробнику простим зрозумілим способом писати код для надсилання REST-запитів на вебсервер. Так, REST-клієнт можна композиційно представити як об’єднання якогось зручного API, транспортного HTTP-клієнта (JDK або зовнішнього) і serializer / deserializer (теж зовнішнього), який перетворюватиме отриманий текст у потрібний формат даних і навпаки. А ще — можливі розширення функціональності, але у будь-якому випадку все починається з HTTP-клієнта.

Почнемо знайомство з найпростішими HTTP-клієнтами з часів Java 1.0. Java ніколи не страждала компактністю і наприклад, в ній не можна отримати вміст Інтернет-ресурсу одним рядком, як у Groovy:

String content = "http://www.google.com".toURL().getText();

Замість цього пропонувалося більш громіздке рішення на основі HttpURLConnection:

       public static String getText(String url) throws Exception {
              URL website = URI.create(url).toURL();
              URLConnection connection = website.openConnection();
              try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
 
                     StringBuilder response = new StringBuilder();
                     String inputLine;
 
                     while ((inputLine = in.readLine()) != null) {
                           response.append(inputLine);
                     }
 
                     return response.toString();
              }
       }

Крім його розміру, такий підхід має й інші недоліки:

  1. Мануальна обробка помилок та читання даних із сервера.
  2. Синхронний блокуючий характер взаємодії із сервером.
  3. Відсутність підтримки пулу потоків.
  4. Відсутність підтримки нових протоколів (HTTP/2+).
  5. Підтримка протоколів, які більше не використовуються (FTP, Gopher та інші).

Зрозуміло, мало хто писав такий код з нуля, а просто використав готову бібліотеку Commons IO або Google Guava. З іншого боку, додавати цілу бібліотеку у свій проєкт заради одного методу — не найоптимальніше рішення. Ще більш монстроїдальний код отримав для POST/PUT-запитів:

       public static String sendText(String url, String data) throws Exception {
              URL website = URI.create(url).toURL();
              URLConnection conn = website.openConnection();
              conn.setDoOutput(true);
              StringBuilder response = new StringBuilder();
 
              try (OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream())) {
 
                     writer.write(data);
                     writer.flush();
                     String line;
                     try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
 
                           while ((line = reader.readLine()) != null) {
                                  response.append(line);
                           }
                     }
                     return response.toString();
              }
       }

Фактично таке рішення — це низькорівневий HTTP-клієнт, який мало підходить для формування REST-запитів. Ситуація дуже довго лишалася на місці, поки в JDK 8/9 не з’явився новий API для роботи з I/O, і вище код можна було переписати як:

URL url = new URL("http://localhost:8080/books");
try (InputStream in = url.openStream()) {
    return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}

Однак він був придатним для найпростіших випадків, а в більш складних доводилося, знову ж таки, використовувати HttpURLConnection. Усе змінилося, коли в Java 11 з’явився довгоочікуваний і повноцінний HTTP-клієнт.

JDK HTTP-клієнт

Коли в 2015 році вийшла специфікація HTTP/2, яку почали жваво підхоплювати браузери, стало зрозуміло, що вдосконалювати та покращувати поточний HTTP-клієнт на базі HttpURLConnection, який створили ще в 90-ті, немає сенсу. Тому в Java 9 як проєкт-інкубатор виник зовсім новий HTTP-клієнт, який мав такі цілі:

  1. Підтримка протоколів HTTP/2 та WebSocket, а також server push.
  2. Використання і синхронного, і асинхронного підходу.
  3. Підтримка сучасних механізмів автентифікації.
  4. Ефективність на рівні конкурентів (Apache HTTP Components, Netty/Jetty-клієнт).

Зрештою після численних переробок він увійшов як стабільна фіча в JDK 11, і зразковий код для відправки GET-запиту виглядає так:

HttpRequest request = HttpRequest.newBuilder()
        .GET()
        .uri(URI.create("http://localhost:8080/books"))
        .build();
HttpResponse<String> response = HttpClient
        .newBuilder()
        .build()
        .send(request, HttpResponse.BodyHandlers.ofString());

Ще одна відмінність від попереднього клієнта — наявність абстракцій, як-от HttpClient, HttpRequest або HttpResponse, які дозволяють писати простіший і зрозуміліший код. У цьому прикладі використовується блокуюча відправка запиту, але всередині HTTP-клієнта запити завжди надсилаються асинхронно, а сама реалізація повністю заснована на CompletableFuture. Більше того, HttpClient містить справжнісінький connection cache, заснований на пулі потоків. Так, якщо ви надсилаєте запити на той самий сервер, немає необхідності щоразу відкривати нове з’єднання. Зрозуміло, ваше з’єднання не триматимуть вічно, воно закривається, якщо довго не використовується (за замовчуванням 20 хвилин).

Утім, усі ці плюшки недоступні користувачам ранніх версій Android, де підтримується лише Java 8, і вони традиційно використовували OkHttp та Apache HTTP Components. Усі інші цілком можуть використовувати JDK HTTP-клієнт, але оскільки в Java досі немає натяку на API для серіалізації даних, доводиться додатково використовувати бібліотеку для серіалізації, а швидше за все повноцінний REST-клієнт, де JDK HTTP-клієнт буде використовуватися для надсилання та отримання даних.

RestTemplate

RestTemplate з’явився у версії Spring 3.0 у 2009 році, коли туди додали підтримку REST-сервісів і відчайдушно потрібен був свій REST-клієнт на заміну не дуже зручних HTTP-клієнтів. Тому з’явився такий клас як RestTemplate, який міг не лише надіслати HTTP-запит, а й серіалізувати відповідь. По суті, він був першим REST-клієнтом у Java-світі.

Оскільки RestTempalte — це потоково-безпечний тип, то ви могли створити його на рівні класу і навіть глобально для всієї програми:

       private final RestTemplate restTemplate = new RestTemplate();

а потім використовувати його API, наприклад, для GET запитів були два методи: getForObject та getForEntity:

public ResponseEntity<Book> findBook(int id) {
       return restTemplate.getForEntity("http://localhost:8080/book/{id}", Book.class, id);
}

Різниця їхня в тому, що один повертав клас-ресурс, а інший — ResponseEntity. Для роботи з HTTP RestTemplate зараз підтримує:

  • JDK-клієнт;
  • Apache HTTP Components;
  • Netty;
  • OKHttp;
  • Vanilla Java.

Останній варіант вибирається за замовчуванням. Але як він виробляє десеріалізацію? Усе завдяки такій інтегрованій абстракції як HttpMessageConverter. Під час ініціалізації RestTemplate перевіряє наявність у classpath відомих йому бібліотек серіалізації:

  • JAXB/JSOB-B;
  • Jackson;
  • Gson.

І якщо вони є, то для десеріалізації використовує перший конвертор, який вміє працювати з цими форматом і типом даних. Аналогічно відбувається й із серіалізацією:

restTemplate.put("http://localhost:8080/book/" + book.getId(), request);

Дуже важливо те, що RestTemplate автоматично відправляє заголовок Content-Type (за замовчуванням application/json), якщо є запит і це об’єкт. За початкового використання RestTemplate можна потрапити в пастку, коли ви хочете отримати від сервера список об’єктів і пишете приблизно такий код:

public List<Book> findBooks() {
       return restTemplate.getForObject("http://localhost:8080/books", List.class);

Такий код скомпілюється і запуститься, але водночас цьому RestTemplate поверне вам не List<Book>, як можна було подумати, а List<Map>, і звичайно ви отримаєте ClassCastException. Тому правильніше буде використовувати не getForObject, а метод exchange, який дозволяє працювати з generic-колекціями:

return restTemplate.exchange("http://localhost:8080/books", HttpMethod.GET,
              null, new ParameterizedTypeReference<List<Book>>() { });

Що з обробкою помилок? Тут за будь-якої помилки від сервера (connection refused, 4xx, 5xx) завжди викидається виняток — або ResourceAccessException чи одне зі спадкоємців RestClientResponseException, навіть якщо ви повертаєте ResponseEntity і за бажання могли отримати інформацію звідти. Тому вам доводиться вибирати один з трьох варіантів:

  1. Обертати всі виклики RestTemplate у try-catch.
  2. Створити об’єкт ResponseErrorHandler і підсунути в RestTemplate.
  3. Додати глобальний @ControllerAdvice.

RestTemplate підтримує Basic Authentication> з коробки, але завдяки такій абстракції як ClientHttpRequestInterceptor можна написати свій код, який буде реалізовувати інші типи аутентифікації.

Які ж загалом недоліки у RestTemplate:

  1. Використовує блокуючий (синхронний) підхід.
  2. Не підтримує streaming.
  3. Використовує перевантаження методів (overloading) для вказівки параметрів, а не більш гнучкий та сучасний варіант builder.
  4. Не пропонує вам якогось контракту, тобто ви можете надіслати запит на будь-який URL і спробувати отримати будь-який тип у відповіді, що збільшує ймовірність помилки.
  5. Перебуває в тому модулі, як і код підтримки серверного web.

Останній недолік є найбільш суттєвим. Якщо ви хочете використовувати RestTemplate у звичайному застосунку, то разом з ним отримаєте модуль spring-web і всі інші залежності з Spring Core. Тобто є сенс використовувати його для застосунків, де вже є Spring MVC. Була пропозиція розділити spring-web на клієнтську та серверну частину, але вона не була ухвалена. Усе це призвело до того, що в Spring 5.0 RestTemplate був переведений в режим підтримки (maintenance), і нових фітч очікувати в ньому не доводиться.

Unirest

У 2012 році компанія Kong, яка відома своїм однойменним API Gateway, вирішила випустити ще один продукт, цього разу REST-клієнт, який вона назвала Unitrest. У ньому вона запропонувала простий зрозумілий API та досить багато можливостей:

Book book = Unirest.get("http://localhost:8080/books/1")
        .asObject(Book.class)
        .getBody();

До плюсів можна віднести невеличкий розмір — всього 200 кілобайт і вбудовану підтримку таких серіалізаторів як-от Jackson і Gson. До відносних мінусів можна зарахувати використання тільки одного HTTP-клієнта від JDK, тому мінімальна версія для запуску Unitrest-JDK 11. Але в Unirest є унікальні можливості, яким він і залучив своїх шанувальників.

Насамперед це вбудований MockClient, який дуже легко налаштувати у тестах:

MockClient client = MockClient.register();
client.expect(HttpMethod.GET)
        .thenReturn(MockResponse.of(500, "internal error"));

Потім підтримка server-side pagination, причому це єдина технологія з усіх розглянутих, яка підтримує це на рівні API:

PagedList<Book> list =  Unirest.get("http://localhost:8080/books")
        .asPaged(
                r -> r.asObject(Book.class),
                r -> r.getHeaders().getFirst("next")
        );

Підтримка кешування відповіді:

Unirest.config().cacheResponses(builder()
        .maxAge(30, TimeUnit.MINUTES));

За промовчанням дані зберігаються в LinkedHashMap, але за бажанням можна адаптувати будь-який з наявних кеш-провайдерів (Caffeine, EhCAche).

Ну і нарешті підтримка різноманітних interceptors, які дозволяють отримати будь-яку інформацію, наприклад, нотифікацію про початок відправки запиту або помилки під час його виконання. Усі ці фічі роблять Unirest цілком привабливим для професійного використання.

JAX-RS клієнт

З Enterprise Java (Java EE/Jakarta EE) ситуація заплутаніша. Довгий час тут взагалі не було свого клієнта REST. І тільки коли в 2009 році в Spring MVC з’явився RestTemplate, справа зрушила з мертвої точки. На жаль, у JAX-RS 1.0, який з’явився того ж року і став частиною Java EE 6, не було додано свого клієнта REST. Тому вендори почали розробляти свої реалізації, наприклад, у Jersey з’явився свій тип Client, робота з яким виглядала так:

Client client = Client.create();
WebResource resource = client.resource("http://localhost:8080/books");
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON).get(ClientResponse.class);
if (response.getStatus() == 200) {
    Book book = response.getEntity(Book.class);

Вона використовувала роботу з HTTP з Vanilla Java і мала досить мізерні можливості серіалізації. Тим часом у 2013 році було випущено Java EE 7, до якого увійшов JAX-RS 2.0, куди вже додали нову специфікацію — клієнт для REST-сервісів. Якщо взяти того ж оновленого JerseyClient, він мав більш потужні можливості, наприклад, підтримував як транспортний протокол:

  1. Grizzly.
  2. Apache HTTP Components.
  3. Helidon HTTP-клієнт.
  4. Jetty HTTP-клієнт.
  5. Netty HTTP-клієнт.
  6. JDK NIO-клієнт.

Утім, за умовчанням використовується та сама Vanilla Java. Сама бібліотечка jersey-client досить невелика (300 КБ), але на додачу до неї йде jersey-common, яка займає 1,2 МБ, тож легковажним такий клієнт не назвеш. І це не враховуючи бібліотеки з серіалізації даних (Jackson, JAXB).

Сам API повністю змінився, якщо порівнювати його з першою версією:

try (Client client = ClientBuilder.newClient()) {
    WebTarget target = client.target("http://localhost:8080/books/1");
    Book book = target.request().accept(MediaType.APPLICATION_JSON).get(Book.class);
    System.out.println(book.getId());
} catch (Exception e) {

Утім його можна назвати кроком уперед порівняно з RestTemplate, оскільки він:

  1. Підтримує builder-style.
  2. Підтримує асинхронну роботу.

Тепер можна було асинхронно надсилати запити та отримувати вже Future, а не просто подання ресурсу:

Future<Book> book = target.request().accept(MediaType.APPLICATION_JSON)
.async().get(Book.class);

Щодо обробки помилок, тут використовується популярний підхід:

  • у разі недоступності сервера викидається IOException.
  • Якщо сервер повернув 4xx/5xx і клієнт запросив подання ресурсу, викидається вимкнення. А якщо клієнт повертає тип Response, він містить усю необхідну інформацію (статус-код, заголовки і тіло запиту).

З відносних мінусів — відсутність такої концепції як ErrorHandler, куди можна було б перенести всю обробку помилок.

Висновки

У попередній статті я розповів про те, як з’явилися перше покоління REST-клієнтів і що передувало цьому. Java пройшла довгий шлях, перш ніж в 11-й версії отримала сучасний HTTP-клієнт. До цього розробникам доводилося використовувати Java-бібліотеки, як-от Apache HTTP Components або OkHttp, якщо вони не хотіли вникати в семантику HttpUrl-Connection.

Розробникам Spring Framework і тут вдалося виявити ініціативу та стати першими, хто створив повноцінний REST-клієнт — RestTemplate. Однак він був жорстко прив’язаний до Spring Framework, тому не став універсальним рішенням, таким як Unirest. Для Enterprise Java у специфікації JAX-RS 2.0 з’явився свій REST-клієнт, реалізований більшістю вендорів (Jersey, RestEasy, Apache CXF). У наступних статтях я продовжу свою розповідь про REST-клієнтів для Java-застосунків.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному9
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Також доступний клієнт okhttp, з версією 4.12.0

Питання не в тому хто кращій, а хто гірший. Питання в тому, хто готовий до віртуальних потоків. Якщо фрейморк ховає конфігурацію тредпулів від розробника, то воно одразу йде на смітник, бо неможливо уявити ситуацію в котрій перевага у ReST-запитах буде надана платформеним потокам.

От вам і питання: хто із цих фремфорків надає змогу користуватися віртуальними потоками через налаштування фабрики потоків, або вміє користуватися батьківським потоком через наслідування (підказка: майже ніхто окрім JDK HTTP).

Абсолютно вірний коментар.
Але я думаю що підтримку швидко підвезуть в найближчих версіях

для мене дивно, що дизайн кожного фремворку робився так, щоб сховати найелементарнішу річ — ExecutorService. Це ж просто база, основа, стовп фреймворків що працюють із сокетами. Наприклад, Netty — одна з найгірших бібліотек і серверів загалом, бо йде суміш JNI та багатопотоковсті.

Для тих, хто не дуже розуміє мого палання стільця. Проблема полягає в тому, що загалом фремворки роблять вигляд що знають краще за розробників. Для мене очевидне краще, а ніж advanced-приховане. Є задачі під які можна створити cached-pool, а не створювати-видаляти потоки кожного разу як це потрібно, бо можна забути про будь-яку C2 оптимізацію. І таких моментів багато (це також стосуються усіляких імплементацій jdbc connection pool).

Фіналочка. Java-розробникик ВЖЕ НЕ КОРИСТУЮТЬСЯ мануальним створенням потоків, для цього є ExecutorService та Future. За пафосними ідеєю створбювати «високорівневі фремворки» ховається бридкий код який ховає базові речі JDK. Так робити не можна.

Обурення розумію, погоджуюсь, але дозвольте запитати. A JDK Stream::parallel не порушує цю концепцію? У нього ж теж, здається, ExecutorService назовні не стирчить?

Тому є деяка причина (най і спірна)
Наскільки я пам’ятаю, стрім параллел всередині працює на Fork Join Framework, який має свій статичний пул тредів 1 на всю жвм.
Прибити метод параллел стрімів до нього має сенс, як дефолтна поведінка щотне потребує налаштувань, а от дати можливість додатково сконфігурувать свій власний пул чи фабрику потоків мало б сенс.

Хоча будем чесними, це далеко не єдине місце в ждк в якому поскупилися на апі.

Якийсь фанатичний мінімалізм давно напрягає. Це як наявність Collection.isEmpty() і паталогічна відсутність .isNotEmpty() що змушує десятиліттями писати свої утільні ліби накшталт apache commons lang

Трохи офф-топік пішов.

Наскільки я пам’ятаю, стрім параллел всередині працює на Fork Join Framework, який має свій статичний пул тредів 1 на всю жвм.

Я теж таке пам’ятаю. Тому, здається, є рекомендація в parallel робити хіба що СPU операціі, тому що I/O може «вичерпати» той єдиний для всіх ForkJoinPool і це заафектить інші parallel стріми. Тут, звісно, треба зауважити, що паралельний I/O — це дивна ідея, і логічніше це робити однією bulk операцією. Коротше кажучи, я parallel взагалі не використовую, і необхідності нема, і не до кінця розумію, як з ним без явного ExecutorService коректно працювати.

Хотілося би думку Дениса Макогона, як причетного до розробки JDK, почути.

наявність Collection.isEmpty() і паталогічна відсутність .isNotEmpty()

З цим теж, звісно, згоден. Було б гарно зробити на рівні мови синтаксичний цукор для кожного isXXX() метода автоматичний isNotXXX, тому що в !longButSelfExplainableObjectName.isFineQualifiedBooleanMethod() знак оклику на початку, особливо біля «l», не видно. Тяжка спадщина assembler/C, 0 — це false, 1 — це true; добре, що хоч boolean завезли.

А мені не вистачає готової обгортки над ExecutorService, яка реалізує концепцію «продюсер-консюмер». Але якоїсь повноцінної, з можливістю нгалаштування розміру кюшки, передачі функцій продюсера і консюмера, їх кількості, інтеррапту і т.д. Коли цю обгортку прийшлося писати самому, вилізла кучка нюансів і прийшлося думати/дебажити імплементацію. І як написати адекватні тести на все це (так нормально і не зробив).
Задачка ніба поширена, а юзати Кафку для цього — IMHO за помірного навантаження оверхед і ускладнення системи.

Я теж сам писав таку штуку. Але не через потребу а просто з цікавості :)

Причина її відсутності в ждк дуже проста — вона потрібна дуже рідко, настільки рідко і з настільки кастомними вимогами що додавати її в стардартну бібліотеку немає сенсу.
Зате різних Exchanger додали, які настільки не потрібні що майже ніхто не пам’ятає що вони є.

Кафку звичайно для такого використовувать це космічний оверкіл )

Я буду радий вам допомогти оформити JEP або тікет якщо буде відповідна мотиваційна частина.

Я теж таке пам’ятаю. Тому, здається, є рекомендація в parallel робити хіба що СPU операціі, тому що I/O може «вичерпати» той єдиний для всіх ForkJoinPool і це заафектить інші parallel стріми.

Так, все вірно. Мова йде про FJP який створюється підчас ініціалізації віртуальної машини, воно зветься Common FJP. Коли мова йде про паралельні потоки, завжди контекст задачі повинен крутитися навколо CPU (математика, вектори і так далі). У цей же час, для віртуальних потоків була створена паралельна інфраструктура — свій FJP (такий самий як і common) і свої підходи по паралельного програмування — structured concurrency.

Що стосується факту, що в паралельних стрімах неможливо вказати із яким ExecutorService працювати, то є простий підхід як вказати який сервіс використовувати:

    var forkJoinPool = new ForkJoinPool(100);
    var primes = forkJoinPool.submit(() ->
        // Parallel task here, for example
        IntStream.range(1, 1000).parallel()
                .filter(PrimesPrint::isPrime)
                .boxed().collect(Collectors.toList())
    ).get();
Тут, звісно, треба зауважити, що паралельний I/O — це дивна ідея

ну, дивного тут нічого нема, повірте, коли мова йде про паралельні стріми, то як ви відмітили це bulk-операції. Але для I/O більш притаманні задачі структурного паралелізму: задачі із «певною» кількістю I/O перемикань (context switch, blocking). Тому й існує StructuredConcurrency як спосіб виконання як bulk-задач, так і послідовних блокувань-очікувань у структурному, а не асинхронному вигляді.

З цим теж, звісно, згоден. Було б гарно зробити на рівні мови синтаксичний цукор для кожного isXXX() метода автоматичний isNotXXX, тому що в !longButSelfExplainableObjectName.isFineQualifiedBooleanMethod() знак оклику на початку, особливо біля «l», не видно. Тяжка спадщина assembler/C, 0 — це false, 1 — це true; добре, що хоч boolean завезли.

Я залюбки допоможу оформити JEP, або тікет на цей функціонал якщо є відповідна мотиваційна частина, а ніж небажання писати знак оклику перед методом =)

що в паралельних стрімах неможливо вказати із яким ExecutorService працювати, то є простий підхід як вказати який сервіс використовувати:

Вельми вдячний, я про таке не знав, дякую за підказку.
Трохи не розумію, як наче термінуюча операція collect всередині Callable лямбди розуміє, що вона всередині ForkJoinPool виконується, але почитаю, розберусь.

Стосовно

автоматичний isNotXXX
Я залюбки допоможу оформити JEP,

Ну, яка може бути мотиваційна частина для синтаксичного цукору? :-)

Компілятору все одно, а людині, здається, легше читати lingam.isNotShort(), бо lingam.isShort() та !lingam.isShort() «виглядають» практично однаково.

(вибачте за вульгарність, це я так, для сміху)

Трохи не розумію, як наче термінуюча операція collect всередині Callable лямбди розуміє, що вона всередині ForkJoinPool виконується, але почитаю, розберусь.

Розібрався

The trick is based on ForkJoinTask.fork which specifies: “Arranges to asynchronously execute this task in the pool the current task is running in, if applicable, or using the ForkJoinPool.commonPool() if not inForkJoinPool()”

stackoverflow.com/a/22269778/5020294

І, так розумію, більш фічаста бібліотека, щоб робити це явно github.com/...​varit/parallel-collectors

Обурення розумію, погоджуюсь, але дозвольте запитати. A JDK Stream::parallel не порушує цю концепцію? У нього ж теж, здається, ExecutorService назовні не стирчить?

Формально, ви праві. Але фактично є можливість обходити це обмеження, бо паралельні стріми «розуміють» із чим вони мають справу. Приклад я навів трохи нижче.

На сьогоднішній момент мабуть найбільш прогресивним та високорівневим підходом до будування REST клієнтів в java є підхід, вперше реалізований у Retrofit square.github.io/retrofit і потім стандартизований у Rest Client for MicroProfile download.eclipse.org/...​rest-client-spec-2.0.html

Підхід дуже схожий на Spring Data — розробник оголошує інтерфейс REST API а фреймворк повертає його реалізацію.

Я не в курсі за Spring boot бо вже пару років як переїхав на Quarkus, який як раз використовує microprofile, але думаю що можна з ним використовувати і Retrofit i якусь з реалізацій microprofile — Jersey або RestEasy

А як же Feign client? Імхо, найкраще рішення.

Зайшов в коменти щоб написати цей комент. Він є вже відносно давно і дуже зручний

Підписатись на коментарі