Старые песни о главном. Java и исходящие запросы

gczoyneiff_giu5osapzaaekbpw.png

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


Задача, которую мы будем решать, предельно проста: нам необходимо отправить GET/POST запрос и получить ответ, который приходит в формате JSON. Чтобы не писать очередной оригинальный микросервис, я воспользуюсь, который предоставляет набор ендпоинтов с некоторыми данными. Все примеры кода максимально упрощены, никаких хитросделанных кейсов с auth токенами и заголовками тут не будет. Только POST и GET, GET и POST, и так 5 раз или около того.
Итак, поехали.
Было бы странно, если бы поставленную задачу нельзя было бы решить без использования сторонних библиотек, конечно можно! (но грустно) Пакет 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 "";
        }


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

[
  {
    "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 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 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 который можно запустить.
Если уйти в сторону от встроенных решений, то первое на что мы наткнемся — http client от Apache. Для доступа нам понадобится сам jar файл, или т.к. я использую Maven то соответствующая зависимость:

 
            org.apache.httpcomponents
            httpclient
            4.5.6
        


И то как выглядят запросы с использованием http client«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 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 есть еще один модуль решающий нашу задачу.

        
            org.apache.httpcomponents
            fluent-hc
            4.5.6
        


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

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

        final Collection 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());


По сути, вызовы схлопнулись в 1 строчку кода, как по мне, это намного более дружелюбно по отношению к разработчикам чем самый первый способ.
Что же дальше? Дальше опыт меня завел в мир 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);


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

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


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

 
            org.springframework.cloud
            spring-cloud-starter-feign
            1.4.5.RELEASE
        


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

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

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

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

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


Красота, не правда ли? И да, те модели данных которые мы писали для resttemplate отлично переиспользуются здесь.
Существует ещё не один способ реализации помимо представленных пяти. Данная подборка это лишь отражение опыта автора, в том порядке в котором я знакомился с ними и начинал использовать в проектах. Сейчас активно пользую feign, радуюсь жизни и жду когда появится ещё что то более удобное, чтобы можно было крикнуть в монитор » выбирают тебя» и все было готово к использованию и интеграции, ну, а пока feign.

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

© Habrahabr.ru