Pular para o conteúdo principal

Integrações

Para fazermos as requisições necessárias para integrações com outras APIs, utilizamos o pacote Feign, um cliente HTTP que nos permite fazer requisições a outros serviços de forma mais fácil que em outros pacotes.

Documentação oficial do Feign (em inglês)

Instalando

pom.xml
    ...
<!-- Core do pacote -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>11.8</version>
</dependency>
<!-- Módulo para utilizar o processador de JSON da Google -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>11.0</version>
</dependency>
<!-- Módulo para adicionar suporte para formulários do tipo
application/x-www-form-urlencoded e multipart/form-data -->
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
...

Definindo requisições a uma API

Para definirmos uma Instância de API, definimos primeiro uma interface contendo endpoints, body da requisição, se necessário, e retorno.

ExemploAPI
public interface ExemploAPI {

@RequestLine("GET /exemplos")
ResponseExemplos getExemplos();

@RequestLine("GET /exemplos/{id}")
ResponseExemplo getExemplo(@Param("id") Integer id);

@RequestLine("POST /exemplos")
@Headers("Content-Type: application/json")
ResponseExemplo sendExemplo(ObjetoEnvio corpo);
}

No código acima:

Linha 3 - É definido na anotação RequestLine o método (GET) e o endpoint sem o domínio (/exemplos);
Linha 4 - Nome do método a ser chamado para executar a requisição (getExemplos) e o tipo de retorno da requisição (ResponseExemplos);

Linha 6 - É definido um endpoint com variável no path (/{id}). Deve ser passado entre chaves ({});
Linha 7 - Para ser passada a variável ao path, devemos inserir nos parâmetros do método com a anotação Param(nome definido na RequestLine).

Linha 10 - Podemos passar Headers necessários na anotação Headers. Neste caso, estamos definindo o header Content-Type com o valor application/json.
Linha 11 - Definimos uma requisição como as outras, mas com método POST e enviando um objeto no body (corpo) da requisição. Para enviar um body, não é necessário passar nenhuma anotação.

Definindo uma instância

Uma instância é um objeto utilizado para realizar requisições com configurações já definidas, como headers ou encoders e decoders, por exemplo. Para construir uma, utilizamos Feign.builder(), e inserimos configurações desejadas em forma de métodos:

No arquivo /services/feign/FeignService, temos a definição de todas as instâncias.
/services/feign/FeignService
@Service
public class FeignService {
// ...
private static final String EXEMPLO_API = "https://linkapi.com.br";

// ...
public ExemploAPI instanceExemploApi (String token) {
ExemploAPI exemploApi = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.errorDecoder(new ExemploDecoder(log))
.requestInterceptor(new ForwardedForInterceptor.InterceptorBuilder()
.authorizationHeader(token)
.contentTypeJsonHeader()
.build())
.target(ExemploAPI.class, EXEMPLO_API);
}
}
No código acima:
Linha 4 - Definimos em uma constante a URL da api que desejamos integrar;
Linha 7 - Método que retorna a instância configurada;
Linha 9 e 10 - Definimos que o encoder e decoder (processadores de objetos json para requisição/resposta) será o GSON, processador
Linha 11 - Definimos um decoder customizado para erros, passando o service de Logs para o registro dos erros.
Linha 12 - Definimos um Interceptor (ver mais) para cada requisição. Neste caso, definimos um token de autorização, Content-Type como JSON, e finalmente, finalizamos a contrução com build().
Linha 16 - Por fim, definimos, no método target, a classe do objeto que contém as requisições e a URL definida na constante.

Utilizando

Para utilizarmos uma instância, criamos um service no caminho /services/api, que conterá todos os métodos relacionados á integração:

/services/api/ExemploAPIService
@Service
public class ExemploAPIService {

@Autowired
private FeignService feignService;

public ExampleReturn enviarRequisicao() {
String token = "token"

try {
ResponseExemplo response = feignService.instanceExemploApi(token).getExemplo(3);
return response;
} catch (Exception e) {
// ... //
}
}
}

No exemplo acima, é passado um token de autorização à instância, e em seguida é chamado o método getExemplo com o parâmetro ID, que buscará na API fictícia o exemplo com ID passado.

Obtendo o token de acesso

Para obter o token de acesso e passá-lo á instância, podemos chamar o método getToken, localizado em OAuthService. Devemos passar como parâmetro o tipo do token que desejamos pegar (ver na classe OAuthType).

Interceptor

O Interceptor customizado utilizado é construído utilizando métodos. Se precisarmos, por exemplo, criar um método que necessite de autorização e tenha o Content-Type como application/json, construíriamos da seguinte forma:

new ForwardedForInterceptor.InterceptorBuilder()
.authorizationHeader(token)
.contentTypeJsonHeader()
.build() // Deve-se sempre terminar com build() para finalizar a construção.

Caso tenha mais algum header que deve ser passado frequentemente, pode-se adicionar à classe InterceptorBuilder.

Adicionando métodos ao interceptor

Para adicionar novos métodos ao construtor do interceptor, deve-se seguir a seguinte estrutura:

// ...
public static class InterceptorBuilder() {
// ...
// Método deve retornar InterceptorBuilder
public InterceptorBuilder headerExemplo() {
/*
- "template" é o responsável por juntar os headers atribuídos para a
construção.
- No método 'header' passamos primeiro o nome do Header a ser atribuído, e
em seguida o valor, que pode ser passado como atributo se necessário.
*/
template.header("Custom-header", "exemplo");
return this;
}
// ...
}
// ...

Decoder de erros

Para cada instância, é possível inserir um errorDecoder, uma classe responsável por padronizar e registrar os erros ocorridos em requisições. A estrutura básica é:

/services/exception/ExemploAPIErrorDecoder
@Component
public class CelcoinAPIErrorDecoder implements ErrorDecoder {

// Service usado para registrar os erros
private LogService log;

public ExemploAPIErrorDecoder(LogService log) {
super();
this.log = log;
}

@Override
public Exception decode(String methodKey, Response response) {
HttpStatus responseStatus = HttpStatus.valueOf(response.status());

InputStream stream = null;
String message = null;
try {
// Tratamento e padronização do erro. Varia para cada API
} catch (IOException | JSONException e) {
message = "ERRO API_EXEMPLO, Status: " + responseStatus + " mensagem: Erro ao salvar mensagem";
}

log.insert(new Log(null, message, new Date()));

return new DataIntegrityException(message);
}
}
Atenção!

Seguindo esta seção, é possível estruturar uma nova integração. Cada API, porém, possui uma implementação e tratamento de erros diferente. É recomendado estudar a API e montar as requisições em uma plataforma de requisições, como o Postman antes de partir para a implementação.