(Источник)

Одна из задач, с которой сталкиваются 99,9% разработчиков, — это обращение к сторонним endpoint’ам. Это могут быть как внешние API, так и «свои» микросервисы. Сейчас все и вся бьют на микросервисы, да. Получить или отправить данные просто, но иногда изобретают велосипеды. Можете назвать 5 способов реализации запросов на Java (c использованием библиотек и без)? Нет — добро пожаловать под кат. Да? Заходите и сравните ;)

0. Intro


Задача, которую мы будем решать, предельно проста: нам необходимо отправить запрос GET/POST и получить ответ, который приходит в формате JSON. Чтобы не писать очередной оригинальный микросервис, я воспользуюсь примером, который предоставляет набор endpoint’ов с некоторыми данными. Все примеры кода максимально упрощены, никаких хитросделанных кейсов с auth-токенами и заголовками тут не будет. Только POST и GET, GET и POST, и так 5 раз или около того.
Итак, поехали.

1. Built-in Java solution


Было бы странно, если бы поставленную задачу нельзя было решить без использования сторонних библиотек. Конечно, можно! Но грустно. Пакет java.net, а именно HttpURLConnection, URL и URLEnconder.

Для отправки запроса, что GET, что POST, необходимо создать объект URL и открыть на его основе соединение:

final URL url = new URL("http://jsonplaceholder.typicode.com/posts?_limit=10");
        final HttpURLConnection con = (HttpURLConnection) url.openConnection();

Далее необходимо сдобрить соединение всеми параметрами:

con.setRequestMethod("GET");
        con.setRequestProperty("Content-Type", "application/json");
        con.setConnectTimeout(CONNECTION_TIMEOUT);
        con.setReadTimeout(CONNECTION_TIMEOUT);

И получить InputStream, откуда уже прочитать все полученные данные.

   try (final BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
            String inputLine;
            final StringBuilder content = new StringBuilder();
            while ((inputLine = in.readLine()) != null) {
                content.append(inputLine);
            }
            return content.toString();
        } catch (final Exception ex) {
            ex.printStackTrace();
            return "";
        }

И, собственно, вот такой ответ мы получим (он будет одинаков для всех последующих примеров, ибо мы работаем с одними и теми же endpoint’ами):

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
...
{
    "userId": 1,
    "id": 9,
    "title": "nesciunt iure omnis dolorem tempora et accusantium",
    "body": "consectetur animi nesciunt iure dolore\nenim quia ad\nveniam autem ut quam aut nobis\net est aut quod aut provident voluptas autem voluptas"
  },
  {
    "userId": 1,
    "id": 10,
    "title": "optio molestias id quia eum",
    "body": "quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error"
  }
]

В случае с POST-запросом все немного сложнее. Мы же хотим не только получить ответ, но и передать данные. Для этого нам нужно их туда положить. Документация нам говорит что это может сработать следующим образом:

final Map<String, String> parameters = new HashMap<>();
        parameters.put("title", "foo");
        parameters.put("body", "bar");
        parameters.put("userId", "1");

        con.setDoOutput(true);
        final DataOutputStream out = new DataOutputStream(con.getOutputStream());
        out.writeBytes(getParamsString(parameters));
        out.flush();
        out.close();

Где getParamsString это простой метод, перегоняющий Map в String, содержащие пары «ключ-значение»:

public static String getParamsString(final Map<String, String> params) {
        final StringBuilder result = new StringBuilder();

        params.forEach((name, value) -> {
            try {
                result.append(URLEncoder.encode(name, "UTF-8"));
                result.append('=');
                result.append(URLEncoder.encode(value, "UTF-8"));
                result.append('&');
            } catch (final UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        });

        final String resultString = result.toString();
        return !resultString.isEmpty()
                ? resultString.substring(0, resultString.length() - 1)
                : resultString;
    }

При успешном создании мы получим объект обратно:

{  "title": "foo",  "body": "bar",  "userId": "1",  "id": 101}

Ссылочка на source, который можно запустить.

2. Apache HttpClient


Если уйти в сторону от встроенных решений, то первое, на что мы наткнемся — HttpClient от Apache. Для доступа нам понадобится сам JAR-файл. Или, так как я использую Maven, то соответствующая зависимость:

 <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>

И то, как выглядят запросы с использованием HttpClient’a, уже намного лучше (source):

 final CloseableHttpClient httpclient = HttpClients.createDefault();

        final HttpUriRequest httpGet = new HttpGet("http://jsonplaceholder.typicode.com/posts?_limit=10");
        try (
                CloseableHttpResponse response1 = httpclient.execute(httpGet)
        ){
            final HttpEntity entity1 = response1.getEntity();
            System.out.println(EntityUtils.toString(entity1));
        }

        final HttpPost httpPost = new HttpPost("http://jsonplaceholder.typicode.com/posts");
        final List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("title", "foo"));
        params.add(new BasicNameValuePair("body", "bar"));
        params.add(new BasicNameValuePair("userId", "1"));
        httpPost.setEntity(new UrlEncodedFormEntity(params));


        try (
                CloseableHttpResponse response2 = httpclient.execute(httpPost)
        ){
            final HttpEntity entity2 = response2.getEntity();
            System.out.println(EntityUtils.toString(entity2));
        }
        httpclient.close();

Мы получили те же данные, но написали при этом вдвое меньше кода. Интересно, куда еще могут завести поиски в таком, казалось бы, базовом вопросе? Но у Apache есть еще один модуль, решающий нашу задачу.

3. Apache Fluent API


        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>fluent-hc</artifactId>
            <version>4.5.6</version>
        </dependency>

И уже с использованием Fluent Api наши вызовы становятся намного читабельнее (source):

        final Content getResult = Request.Get("http://jsonplaceholder.typicode.com/posts?_limit=10")
                .execute().returnContent();
        System.out.println(getResult.asString());

        final Collection<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("title", "foo"));
        params.add(new BasicNameValuePair("body", "bar"));
        params.add(new BasicNameValuePair("userId", "1"));

        final Content postResultForm = Request.Post("http://jsonplaceholder.typicode.com/posts")
                .bodyForm(params, Charset.defaultCharset())
                .execute().returnContent();
        System.out.println(postResultForm.asString());

И как бонус — пример. Если мы хотим передавать данные в боди не как форму, а как всеми любимый JSON:

        final Content postResult = Request.Post("http://jsonplaceholder.typicode.com/posts")
                .bodyString("{\"title\": \"foo\",\"body\":\"bar\",\"userId\": 1}", ContentType.APPLICATION_JSON)
                .execute().returnContent();
        System.out.println(postResult.asString());

По сути вызовы схлопнулись в одну строчку кода. Как по мне, это намного более дружелюбно по отношению к разработчикам, чем самый первый способ.

4. Spring RestTemplate


Что же дальше? Дальше опыт меня завел в мир Spring. И, что не удивительно, у спринга тоже имеются инструменты для решения нашей простенькой задачи (странно, правда? Задача, даже не так — потребность! — базового уровня, а решений зачем-то больше одного). И первое же решение (базовое), которое вы найдете в экосистеме Spring, это RestTemplate. И для этого нам нужно тянуть уже немалую часть всего зоопарка. Так что если вам нужно отправить запрос в НЕспринговом приложении, то ради этого лучше не тянуть всю кухню. А если спринг уже есть, то почему бы и да? Как притянуть все, что необходимо для этого, можно посмотреть здесь. Ну а собственно GET-запрос с использованием RestTemplate выглядит следующим образом:

        final RestTemplate restTemplate = new RestTemplate();

        final String stringPosts = restTemplate.getForObject("http://jsonplaceholder.typicode.com/posts?_limit=10", String.class);
        System.out.println(stringPosts);

Гуд. НО! Работать со строкой уже не хочется, тем более есть возможность получать не строки, а готовые объекты, которые мы ожидаем получить! Создаем объект Post:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Builder
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public class Post {

    private int id;
    private String title;
    private String body;
    private int userId;

    public String toString() {
        return String.format("\n id: %s \n title: %s \n body: %s \n userId: %s \n", id, title, body, userId);
    }
}

Здесь:
Builder, Getter, Setter — сахар от Lombok, чтобы не писать все руками. Да, вот она, лень-матушка.
JsonIgnoreProperties — чтобы в случае получения неизвестных полей не вылетать в ошибку, а использовать те поля, которые нам известны.
Ну и toString, чтобы выводить наши объекты в консоль, и это можно было прочитать. Ну и собственно наши GET- и POST- запросы перевоплощаются в (source):

        // Map it to list of objects
        final Post[] posts = restTemplate.getForObject("http://jsonplaceholder.typicode.com/posts?_limit=10", Post[].class);
        for (final Post post : posts) {
            System.out.println(post);
        }

        final Post postToInsert = Post.builder()
                .body("bar")
                .title("foo")
                .userId(1)
                .build();

        final Post insertedPost = restTemplate.postForObject("http://jsonplaceholder.typicode.com/posts", postToInsert, Post.class);
        System.out.println(insertedPost);

И у нас уже в руках объекты, а не строка, которую надо разбирать самостоятельно.

Кул. Теперь мы можем написать некоторую обертку вокруг RestTemplate, чтобы запрос строился корректно. Выглядит не так уж плохо, но, как по мне, это можно еще улучшить. Чем меньше кода пишется, тем меньше вероятность ошибки. Все же знают, что основная проблема зачастую PEBCAK (Problem Exists between Chair and Keyboard)…

5. Spring Feign


И тут на сцену выходит Feign, который входит в состав Spring Cloud. Сначала добавим к уже добавленному ранее спринговому окружению Feign-зависимость:

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.5.RELEASE</version>
        </dependency>

По сути все, что надо, это объявить интерфейс и сдобрить его хорошей жменькой аннотаций. Особенно данный подход будет симпатичен тем, кто пишет контроллеры с использованием спринга.

Вот что нам надо сделать для отправки запросов посредством Feign (source).

@FeignClient(name = "jsonplaceholder", url = "http://jsonplaceholder.typicode.com", path = "/posts")
public interface ApiClient {

    @RequestMapping(method = GET, value = "/", consumes = APPLICATION_JSON_VALUE)
    List<Post> getPosts(@RequestParam("_limit") final int postLimit);

    @RequestMapping(method = POST, value = "/", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
    Post savePost(@RequestBody Post post);
}

Красота, не правда ли? И да, те модели данных, которые мы писали для RestTemplate, отлично переиспользуются здесь.

6. Conclusion


Существует еще не один способ реализации помимо представленных пяти. Данная подборка это лишь отражение опыта автора в том порядке, в котором я знакомился с ними и начинал использовать в проектах. Сейчас активно пользую Feign, радуюсь жизни и жду, когда появится еще что-то более удобное, чтобы можно было крикнуть в монитор «<название библиотеки>, выбираю тебя!» — и все было готово к использованию и интеграции. Ну а пока Feign.

P.S. Как один из «ленивых» способов можно рассматривать генерируемый Swagger клиент. Но, как говорится, есть нюанс. Далеко не все разработчики используют Swagger для документирования своих API, и еще меньше делают это настолько качественно, чтобы можно было спокойно сгенерировать и использовать клиент, а не получить вместо него энтомологическую коллекцию, от которой будет больше вреда, чем пользы.

Комментарии (18)


  1. yaushev_st
    17.09.2018 20:51

    Буквально сегодня стояла задача сделать запрос сервлету и распарсить ответ. Использовал 1ый способ, завтра попробую остальные. Спасибо.


    1. viktoriashebetina Автор
      17.09.2018 21:05

      Пожалуйста! Успехов ;)


  1. Chris_Griffin
    17.09.2018 21:31
    +1

    Swagger, по-моему личному опыту, это лютая боль в нижней части спины.
    Перегенерация клиентов на любое изменение апи, причем кривыми инструментами со странными зависимостями, отсутствие поддержки предыдущих версий API, и прочая прочая.

    Есть тут люди которым он зашел? Может быть у нас его готовят неправильно.


    1. mmMike
      18.09.2018 06:32

      После того, как мне пришлось влазить в исходники swagger и править там кусочек, что бы получить сгенеренный корректный код клиента, я решил, что не поленюсь руками код писать. По крайней мере это более предсказуемо по срокам.


      Авторы интерфейса сказали "а мы swagger используем только для документирования и у нас проблем нет".


      Уже не помню в чем там была проблема… года 2 назад было. Но осадочек остался.


    1. TSergey_tm
      18.09.2018 10:36

      Есть.

      Только ведь не важно что будет (swagger, WSDL и т.п.) если проблема в:

      Перегенерация клиентов на любое изменение апи, ..., отсутствие поддержки предыдущих версий API


      У нас приняты правила:
      1. мажорная версия АПИ в урле
      2. в рамках мажорной версии данной функции обратная совместимость обязательна
      3. параметры, которые добавляются в запросы в рамках минорных версий, не могут быть обязательными
      4. клиенты и серверы толерантны к данным, которые не оговорены в АПИ


      Таким образом, из-за пункта 1 любое несовместимое изменение просто добавляется в версию выше по другому урлу. Плюс: клиентскую часть не требуется перегенерировать с выпученными глазами. Плюс: Не требуется проводить синхронных установок на ребочку и т.д.

      Из-за пунктов 2 и 3, при поднятии минорной версии, клиентов не требуется перегенерировать, если им не требуется новые параметры. Плюс: можно продолжать работать на старых клиентах и всё должно работать.

      Плюс: из-за пункта 4 любой из участников может обновиться независимо от других. Если у клиент не использует новые функции, то он может уехать на рабочку раньше сервера и это не сломает обмен. Может сервер уехать на рабочку раньше клиентов и новые параметры возвращаемых значений не сломают клиента.

      Из минусов:
      1. программистам работать (а точнее думать) приходится.
        Надо держать совместимость старых версий до какого-то срока (обычно полгода-год). Надо писать код, который не уйдёт в разнос, если появились какие-то данные или неожиданные возвращаемые значения.
      2. нужна строгая дисциплина. Ну тут ревьюверы помогут, ибо если они пропустят изменение, нарушающее правила, то им же и придётся судорожно обновлять все системы


  1. z0mb1ek
    17.09.2018 21:36

    А еще можно взять OkHttp Client и обернуть в JsonNode(Jackson)


    1. aleksandrzhmydyak
      17.09.2018 22:22

      OkHttp + Retrofit


  1. Portnov
    17.09.2018 21:38
    +2

    >> Одна из задач с которой сталкивается 99.9% разработчиков — это X
    Очень умиляют такие обобщения.


    1. Danik-ik
      19.09.2018 17:25

      Сенсация! Уже 100% россиян подключены к интернету! Таковы результаты опроса, проведённого на днях в интернете...


      Он же не уточнил, о каких разработчиках идёт речь? Вот из них и будут те проценты. Простим его. Сочтём гиперболой.


  1. arrhangel
    18.09.2018 10:55

    почему нет jboss resteasy?


  1. Starkom
    18.09.2018 14:40

    А если внешний API не REST, а GraphQL? Тот же RestTemplate становится совсем неудобен.


  1. ris58h
    18.09.2018 14:40

    Как можно было забыть о Retrofit? Очень популярная библиотека (звезд на github больше чем у Spring Boot). Пишешь интерфейс + пару аннотаций и вперёд!


  1. gerzog
    18.09.2018 15:07

    Если уж на то пошло, то начиная с 9ой версии — в Java добавили HTTP Client — так что способ через HttpURLConnection вообще можно считать устаревшим


  1. JustSokol
    18.09.2018 15:07

    еще можно curl дернуть


    1. Sigest
      19.09.2018 14:50

      Да можно вообще напрямую с сетевой картой работать, байтики туда сюда гонять, чоуж там.


  1. sedyh
    18.09.2018 19:08

    Если не придется делать общий пул или держать несколько клиентов, то можно попробовать Unirest, это обертка над apache http components.
    Но у него есть два существенных минуса:


    • Все написано на статиках с единственным экземпляром синхронного и асинхронного клиента.
    • Долгое время не обновлялся.

    Для решения этих проблем существует форк с объектным стилем, однако если они появятся, то проще обернуть http client самому.


  1. Throwable
    18.09.2018 20:42

    Немного в сторону: если кому надо SOAP дергать, вместо тяжелых Apache CXF и JAX-WS пригодится очень лайтовая библиотечка: https://github.com/reficio/soap-ws


  1. Andrey_V_Markelov
    19.09.2018 10:09

    Все-таки не хватает github.com/AsyncHttpClient/async-http-client в списке