Java HTTP Clients — As várias formas de chamar uma API REST em Java

Archimedes Fagundes Junior
10 min readMar 31, 2023

--

Diversos cabos de fibra ótica, representando as diversas conexões possíveis com a internet.
Photo by JJ Ying on Unsplash

Uma das grandes vantagens de desenvolver em Java é a quantidade maciça de bibliotecas e frameworks disponíveis para executar qualquer tarefa necessária. Isso se deve a uma comunidade muito ativa e dedicada à linguagem criada há quase 30 anos.

No entanto, uma das desvantagens é que podemos nos sentir um pouco sobrecarregados com as diferentes implementações, configurações, formas de utilização e boas práticas. Isso também se aplica às chamadas de APIs REST (ou qualquer outro tipo de requisição HTTP), pois há muitas bibliotecas para escolher: HttpURLConnection, HttpClient, RestTemplate e WebClient do Spring, Spring Cloud OpenFeign, entre outras.

O objetivo do artigo é apresentar exemplos simples de duas implementações para cada uma dessas bibliotecas — uma solicitação GET e uma POST — para criar um “cheat sheet” para consultas futuras.

Todos os exemplos estão no repositório https://github.com/afagundes/java-http-clients.

Bora lá 😎

Pré-requisitos

First things first.

Todos os exemplos abaixo consultarão a mesma API disponível em https://jsonplaceholder.typicode.com/users. Aliás, recomendo bastante esse site para quem estiver estudando REST e criando as primeiras requisições. Tem vários endpoints disponíveis lá.

O endpoint escolhido retorna uma lista de usuários no seguinte formato:

[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
},
... vários outros usuários

Nós queremos consultar a API e converter o JSON retornado para um objeto. Para isso vamos adicionar a seguinte biblioteca no nosso pom.xml:

<dependencies>  
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
</dependencies>

Se você estiver usando Gradle, é um pouquinho menos verboso 😅:

dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2'
}

Em seguida vamos criar um record (para quem estiver usando o JDK 14+, se você estiver usando uma versão anterior vai ter que criar um POJO, como faziam os nossos ancestrais):

// User.java

public record User(
Integer id,
String name,
String username,
String email,
Address address,
String phone,
String website,
Company company)
{
public record Address(
String street,
String suite,
String city,
String zipcode,
Geo geo)
{
public record Geo(String lat, String lng) {}
}

public record Company(String name, String catchPhrase, String bs) {}
}

E, por fim, para facilitar a nossa vida, vamos criar uma classe utilitária que contenha alguns métodos para converter um objeto JSON para o formato de objeto e vice-versa, entre outras funcionalidades:

// ExampleUtils.java

public class ExampleUtils {

private ExampleUtils() {}

public static final String USER_API =
"https://jsonplaceholder.typicode.com/users";
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

public static List<User> toList(InputStream inputStream) {
try {
return OBJECT_MAPPER.readValue(
inputStream, new TypeReference<>() {});
}
catch (IOException exc) {
throw new UncheckedIOException(exc);
}
}

public static User toObject(InputStream inputStream) {
try {
return OBJECT_MAPPER.readValue(inputStream, User.class);
}
catch (IOException exc) {
throw new UncheckedIOException(exc);
}
}

public static String toJson(User user) {
try {
return OBJECT_MAPPER.writeValueAsString(user);
}
catch (JsonProcessingException exc) {
throw new UncheckedIOException(exc);
}
}

public static User buildUser() {
User.Address address = new User.Address(
"Rua http 200",
"apto POST",
"São Paulo",
"00200-404",
new User.Address.Geo("-257422", "25566987"));

User.Company company = new User.Company(
"My Great Company",
"We develop software!",
"sofware, development, java");

return new User(null,
"Archimedes Fagundes Junior",
"archimedes.junior",
"archimedes.junior@dev.com",
address,
"11 95523-9999",
"https://my.company.com",
company);
}
}

Maravilha! Sem mais enrolações, vamos para a nossa primeira implementação: A classe HttpURLConnection.

Old School HttpURLConnection

A primeira implementação a ser explorada é a HttpURLConnection. Essa implementação é a mais antiga e está presente desde a JDK 1.1. Abaixo está um exemplo de um método que exibe uma lista de usuários usando o método GET e a classe auxiliar:

public void listUsers() {  
System.out.println("Listing users using HttpURLConnection:");

try {
URL url = new URL(ExampleUtils.USER_API);
HttpURLConnection httpURLConnection = (HttpURLConnection) url
.openConnection();
httpURLConnection.setRequestMethod("GET");
int responseCode = httpURLConnection.getResponseCode();

System.out.println("HTTP status: " + responseCode);
System.out.println("Users returned in request: ");

List<User> users = ExampleUtils.toList(httpURLConnection
.getInputStream());
users.forEach(System.out::println);

System.out.println("Headers:");
httpURLConnection.getHeaderFields().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}
catch (MalformedURLException e) {
throw new RuntimeException("You've entered an invalid URL here: "
+ ExampleUtils.USER_API);
}
catch (IOException e) {
throw new RuntimeException("Error processing request", e);
}
}

Observe a linha que contém o trecho List<User> users = ExampleUtils.toList(httpURLConnection.getInputStream()). Aqui um InputStream é obtido e passado para nosso método auxiliar. Não há métodos para obter o JSON diretamente como uma string. Vamos seguir usando a abordagem do InputStream para os outros exemplos também.

Agora, vamos tentar criar um usuário usando o método POST e exibir o retorno:

public void createNewUser() {  
System.out.println("Creating a new user using HttpURLConnection:");
User user = ExampleUtils.buildUser();

try {
URL url = new URL(ExampleUtils.USER_API);
HttpURLConnection httpURLConnection = (HttpURLConnection) url
.openConnection();
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setRequestProperty(
"Content-Type", "application/json");

String userJson = ExampleUtils.toJson(user);

httpURLConnection.setDoOutput(true);
try (OutputStream outputStream = httpURLConnection.getOutputStream()) {
outputStream.write(userJson.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}

int responseCode = httpURLConnection.getResponseCode();
System.out.println("HTTP status: " + responseCode);

User createdUser = ExampleUtils.toObject(httpURLConnection
.getInputStream());
System.out.println("Created new user: " + createdUser);

System.out.println("Headers:");
httpURLConnection.getHeaderFields().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}
catch (MalformedURLException e) {
throw new RuntimeException("You've entered an invalid URL here: "
+ ExampleUtils.USER_API);
}
catch (IOException e) {
throw new RuntimeException("Error processing request", e);
}
}

Dois pontos interesantes a serem observados:

  1. Adicionamos um novo cabeçalho à requisição usando httpURLConnection.setRequestProperty("Content-Type", "application/json")
  2. Para enviarmos algum valor no corpo da requisição, precisamos primeiro habilitar o output com httpURLConnection.setDoOutput(true), obter uma referência ao objeto OutputStreame escrever o valor nele. Cansativo.

Felizmente, há uma solução mais moderna: o HttpClient do Java 11.

Java 11 HttpClient

O JDK 11, lançado em Setembro de 2018, trouxe um novo cliente HTTP mais simples e fácil de utilizar. Vamos dar uma olhada em como listar os usuários do endpoint:

public void listUsers() {  
System.out.println("Listing users using Java 11 HttpClient:");

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(
URI.create(ExampleUtils.USER_API)).GET().build();

try {
HttpResponse<InputStream> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofInputStream());

int statusCode = response.statusCode();
System.out.println("HTTP status: " + statusCode);

System.out.println("Users returned in request: ");
List<User> users = ExampleUtils.toList(response.body());
users.forEach(System.out::println);

System.out.println("Headers:");
response.headers().map().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}
catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}

O exemplo está obtendo o retorno da API como um `InputStream` para que possamos continuar usando nossos métodos auxiliares, mas se quisermos pegar o JSON como uma String, podemos fazer:

HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println("Response json: " + response.body());

Agora vamos ver como criar um novo usuário:

public void createNewUser() {  
System.out.println("Creating a new user using Java 11 HttpClient:");

User user = ExampleUtils.buildUser();
HttpRequest.BodyPublisher userPublisher = HttpRequest
.BodyPublishers.ofString(ExampleUtils.toJson(user));

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest
.newBuilder(URI.create(ExampleUtils.USER_API))
.POST(userPublisher)
.setHeader("Content-Type", "application/json")
.build();

try {
HttpResponse<InputStream> response = httpClient
.send(request, HttpResponse.BodyHandlers.ofInputStream());

int statusCode = response.statusCode();
System.out.println("HTTP status: " + statusCode);

User createdUser = ExampleUtils.toObject(response.body());
System.out.println("Created new user: " + createdUser);

System.out.println("Headers:");
response.headers().map().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}
catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}

Bem mais claro e fluente que sua classe avó 😁

No exemplo acima, fizemos uma requisição POST síncrona, ou seja, nossa aplicação ficará parada aguardando o retorno da API. Caso queiramos fazer uma chamada assíncrona utilizando a classe antiga HttpURLConnection, precisaremos criar uma nova thread e executar nosso código nela. Felizmente, a nova classe HttpClient permite realizar chamadas assíncronas sem a necessidade de esforço adicional. O trecho de código a seguir apresenta um exemplo de criação de um novo usuário de forma assíncrona:

public void createNewUserAsync() {  
System.out.println("Creating a new user asynchronously using Java 11 HttpClient:");

User user = ExampleUtils.buildUser();
HttpRequest.BodyPublisher userPublisher = HttpRequest
.BodyPublishers.ofString(ExampleUtils.toJson(user));

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest
.newBuilder(URI.create(ExampleUtils.USER_API))
.POST(userPublisher)
.setHeader("Content-Type", "application/json")
.build();

httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
.thenApply(response -> {
System.out.println("Http status: " + response.statusCode());
System.out.println("Headers:");
response.headers().map().forEach((header, value) -> System.out.println(header + " = " + String.join(", ", value)));
return response;
})
.thenApply(HttpResponse::body)
.thenApply(ExampleUtils::toObject)
.thenAccept(createdUser -> System.out.println("New user created asynchronously: " + createdUser))
.join();
}

OK. Vimos as implementações disponíveis na API do Java. Agora vamos dar uma olhada em três bibliotecas, uma muito conhecida, as outras duas nem tanto. Vamos começar pela mais conhecida.

Spring RestTemplate

Projetos criados utilizando Spring Boot web já possuem na caixa de ferramentas a classe RestTemplate disponível para uso. Vamos ver como pegar a lista de usuários com ela:

public void listUsers() {  
System.out.println("Listing users using Spring RestTemplate:");

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<User[]> response = restTemplate
.getForEntity(URI.create(ExampleUtils.USER_API), User[].class);

HttpStatusCode statusCode = response.getStatusCode();
System.out.println("HTTP status: " + statusCode.value());
System.out.println("Is status code 2xx successful? "
+ statusCode.is2xxSuccessful());

System.out.println("Users returned in request: ");
User[] users = Objects.requireNonNull(response.getBody());
Arrays.stream(users).forEach(System.out::println);

System.out.println("Headers:");
response.getHeaders().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}

E como criar um novo usuário na nossa API:

public void createNewUser() {  
System.out.println("Creating a new user using Spring RestTemplate:");

User user = ExampleUtils.buildUser();

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<User> response = restTemplate
.postForEntity(URI.create(ExampleUtils.USER_API), user, User.class);

HttpStatusCode statusCode = response.getStatusCode();
System.out.println("HTTP status: " + statusCode.value());
System.out.println("Is status code 2xx successful? "
+ statusCode.is2xxSuccessful());

User createdUser = response.getBody();
System.out.println("Created new user: " + createdUser);

System.out.println("Headers:");
response.getHeaders().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}

Note que dessa vez não precisamos usar nossos métodos auxiliares para converter o InputStream ou String no objeto User. O RestTemplate já cuida dessa parte para nós.

Nos exemplos acima usamos os métodos getForEntity() e postForEntity() para interagir com a API. Também podemos usar o método mais genérico exchange. Vamos ver como criar um novo usuário com esse método:

public void createNewUserUsingExchangeMethod() {  
System.out.println("Creating a new user using Spring RestTemplate's exchange method:");

RestTemplate restTemplate = new RestTemplate();

RequestEntity<User> request = RequestEntity
.post(URI.create(ExampleUtils.USER_API))
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer XYZ1234abc")
.body(ExampleUtils.buildUser());

ResponseEntity<User> response = restTemplate
.exchange(request, User.class);

HttpStatusCode statusCode = response.getStatusCode();
System.out.println("HTTP status: " + statusCode.value());
System.out.println("Is status code 2xx successful? "
+ statusCode.is2xxSuccessful());

User createdUser = response.getBody();
System.out.println("Created new user with exchange method: " + createdUser);

System.out.println("Headers:");
response.getHeaders().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}

Spring WebClient

Ainda dentro do universo do Spring Boot, mas dessa vez para os que trabalham com código reativo usando a biblioteca Spring WebFlux, temos a classe WebClient. Agora nosso código vai ficando cada vez menos verboso.

Vamos listar os usuários usando o WebClient:

public void listUsers() {  
System.out.println("Listing users using Spring WebFlux:");

WebClient webClient = WebClient.create(ExampleUtils.USER_API);
ResponseEntity<List<User>> response = webClient.get()
.retrieve()
.toEntityList(User.class)
.block();

Objects.requireNonNull(response);
HttpStatusCode statusCode = response.getStatusCode();
System.out.println("HTTP status: " + statusCode.value());
System.out.println("Is status code 2xx successful? "
+ statusCode.is2xxSuccessful());

System.out.println("Users returned in request: ");
List<User> users = Objects.requireNonNull(response.getBody());
users.forEach(System.out::println);

System.out.println("Headers:");
response.getHeaders().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}

Pouquíssimas linhas de configuração. Agora está ficando bom de verdade 😁.

Vamos ver como criar um novo usuário:

public void createNewUser() {  
System.out.println("Creating a new user using Spring WebClient:");

User user = ExampleUtils.buildUser();
WebClient webClient = WebClient.create(ExampleUtils.USER_API);
ResponseEntity<User> response = webClient.post()
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(user), User.class)
.retrieve()
.toEntity(User.class)
.block();

Objects.requireNonNull(response);
HttpStatusCode statusCode = response.getStatusCode();
System.out.println("HTTP status: " + statusCode.value());
System.out.println("Is status code 2xx successful? "
+ statusCode.is2xxSuccessful());

User createdUser = response.getBody();
System.out.println("Created new user: " + createdUser);

System.out.println("Headers:");
response.getHeaders().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}

O WebClient é uma biblioteca reativa que trabalha com fluxos de dados, como Flux e Mono, por padrão. Para os nossos exemplos simples, usamos o método block() para fazer com que o cliente espere até que todos os dados da requisição sejam recebidos antes de retornar as entidades. No entanto, se você quiser se aprofundar em Spring WebFlux e entender o que são Flux e Mono, recomendo dar uma olhada na documentação oficial disponível em https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux.

Spring Cloud OpenFeign

Agora, vamos subir alguns degraus na escada da abstração e usar uma biblioteca chamada OpenFeign, que foi integrada aos módulos do Spring Cloud.

O conceito é extremamente simples: você criar um interface, adiciona algumas anotações e voilá, temos um cliente HTTP funcional. O Spring cuida da parte chatas.

Antes de usar o Feign client, precisamos habilitá-lo. Para isso, adicionamos uma anotação na classe principal de nossa aplicação:

@SpringBootApplication  
@EnableFeignClients
public class HttpClientFeignApplication {

public static void main(String[] args) {
SpringApplication.run(HttpClientFeignApplication.class, args);
}

}

Repare naquele @EnableFeignClients lá em cima.

Agora, vamos criar nossa interface responsável por se comunicar com a API de usuários:

// UserClient.java

@FeignClient(name="userClient", url = ExampleUtils.USER_API)
public interface UserClient {

@GetMapping
List<User> simpleListUsers();

@GetMapping
ResponseEntity<List<User>> listUsers();

@PostMapping
ResponseEntity<User> createNewUser(User user);

}

E é exatamente isso e nada mais! Aqui criamos três métodos:

  1. O método simpleListUsers() apenas nos retorna a lista de usuários, como se fosse uma chamada de método qualquer.
  2. O método listUsers() nos retorna um objeto do tipo ResponseEntity para que possamos inspecionar os headers, status da requisição, etc.
  3. O método createNewUser() para criar um novo usuário.

Vamos ver como usar cada um deles.

Mas antes, não esqueça de injetar o client como uma dependência da sua classe:

@Component  
public class HttpClientFeign {

private final UserClient userClient;

public HttpClientFeign(UserClient userClient) {
this.userClient = userClient;
}

// ... other methods listed below

Retornando uma simples lista de usuários:

public void simpleListUsers() {  
System.out.println("\nListing users using OpenFeign client:");
List<User> users = userClient.simpleListUsers();

System.out.println("Users returned in request: ");
users.forEach(System.out::println);
}

Retornando a lista de usuários e informações sobre a requisição:

public void listUsers() {  
System.out.println("\nListing users using OpenFeign client:");
ResponseEntity<List<User>> response = userClient.listUsers();

int statusCode = response.getStatusCode().value();
System.out.println("HTTP status: " + statusCode);

System.out.println("Users returned in request: ");
List<User> users = Objects.requireNonNull(response.getBody());
users.forEach(System.out::println);

System.out.println("Headers:");
response.getHeaders().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}

E, por fim, criando um novo usuário:

public void createNewUser() {  
System.out.println("\nCreating a new user using OpenFeign client:");

User user = ExampleUtils.buildUser();
ResponseEntity<User> response = userClient.createNewUser(user);

int statusCode = response.getStatusCode().value();
System.out.println("HTTP status: " + statusCode);

User createdUser = response.getBody();
System.out.println("Created new user: " + createdUser);

System.out.println("Headers:");
response.getHeaders().forEach((header, value) ->
System.out.println(header + " = " + String.join(", ", value)));
}

Simples e extremamente prático!

Conclusão

Esse artigo não tem a pretensão de ser conclusivo, mas sim um guia de consulta rápida para as bibliotecas que uso em meus projetos. Há ainda muito mais recursos pra explorar dentro dessas bibliotecas. Também não mencionei outras bibliotecas interessantes, como o Http Client do Micronaut ou da Apache Commons, mas vou deixar uns links pra quem tiver interesse.

Como dito acima, todos esses exemplos, incluindo suas configurações e classes adicionais, estão lá no meu Github.

Até a próxima 🚀

Referências

--

--

Archimedes Fagundes Junior

Senior softwarer engineer with great passion for coding, music and traveling.