Utilizando Apachel Camel para agregar endpoints de REST APIs
André Luiz de Gusmão
Posted on July 27, 2020
O que é o Apache Camel
Conforme a descrição do próprio criador Claus Ibsen, disponível no livro Camel in Action (tradução livre):
O cerne do Camel framework é ser um mecanismo de roteamento ou mais precisamente, um framework que possibilita a construção de rotas. Ele permite definir regras customizadas de roteamento, decidir quais de quais fontes aceitar mensagens e como processar e enviar essas mensagens para outros destinos. O Camel usa uma linguagem de integração que permite definir regras complexas de roteamento, similar a processos de negócio. Como monstrado na figura abaixo, ele pode ser o elo que junta sistemas distintos.
Um dos principios fundamentais do Camel é que ele faz deduções sobre os tipos de dados que você precisa processar. Isso é muito importante porque oferece ao desenvolvedor a oportunidade de integrar qualquer tipo de sistema, sem precisar converter os dados para um formato específico.
Dentre diversas soluções de integração que podemos montar com o Camel, neste artigo, iremos construir um agregador de chamadas a APIs, utilizando o EIP Enricher.
Os códigos apresentados neste artigo podem ser encontrados em https://github.com/andredgusmao/camel-rest-aggregator
O exemplo
Para mostrar como o EIP Enricher pode ser utilizado no Camel vamos disponibilizar uma aplicação que agrega chamada de duas APIs:
- API de autores de livros com os endpoints:
[GET] /authors
[GET] /authors/{name}
- API de livros com o endpoint:
[GET] /books/{authorId}
Nossa aplicação Camel vai disponibilizar dois endpoints:
-
[GET] /integration/authors
- Consulta a API de autores. -
[GET] /integration/authors/{name}
- Consulta na API de autores o autor pelo nome (ex:jr-tolkien
,jk-rowling
) e enriquece a resposta com a consulta de todos os livros do autor na API de livros.
A aplicação Camel
Criando o projeto
Para criar o projeto basta acessar https://start.spring.io/ e preencher a área de project metadata com:
Spring Boot: 2.3.1
Group: br.com.camel.bookstore
Artifact: camel-bookstore-aggregator
Version: 1.0
Name: camel-bookstore-aggregator
Dependencies: 'Apache Camel'
Clique em Generate
para criar o projeto e então edite o pom para adicionar as dependências que serão utilizadas (undertow, rest e http)
o pom do projeto deve ficar algo similar a:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>br.com.camel.bookstore</groupId>
<artifactId>camel-bookstore-aggregator</artifactId>
<version>1.0</version>
<name>camel-bookstore-aggregator</name>
<properties>
<java.version>1.8</java.version>
<camel.version>3.1.0</camel.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-rest-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-undertow</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-http</artifactId>
<version>${camel.version}</version>
</dependency>
...
</dependencies>
</project>
A rota Rest
Vamos criar duas classes com rotas Camel. Na classe RestRoute
estarão os endpoints e na classe RestAggregatorRoute
estarão as rotas que acessam as APIs e enriquecem o conteúdo.
@Component
public class RestRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
//define as configurações do servidor como endereço de host e porta
restConfiguration()
.host("0.0.0.0").port(8080)
.bindingMode(RestBindingMode.auto);
//inicia delaração dos serviços REST
rest("/integration")
//Endpoint que consulta todos os autores
.get("/authors")
.route().routeId("rest-all-authors")
.to("direct:call-rest-all")
.endRest()
//Endpoint que usa o Enrich EIP
.get("/authors/{name}")
.route().routeId("rest-author-by-name")
.to("direct:call-rest-author")
.endRest();
}
}
Os endpoints [GET] /integration/authors
e [GET] /integration/authors/{name}
ao serem invocados chamam as rotas direct:call-rest-all
e direct:call-rest-author
que serão definidas na outra classe.
A rota de integração
Consulta a API de autores
A rota que chama todos os autores utiliza o componente http
para consumir o endpoint da API de autores.
from("direct:call-rest-all")
.routeId("all-service")
.removeHeaders("CamelHttp*")
.setHeader(Exchange.HTTP_METHOD, constant("GET"))
.to("http://{{authors.url}}/authors");
Na rota é utilizado o removeHeaders("CamelHttp*")
para garantir que na chamada da API teremos apenas headers do componente http
, vamos usar HTTP_METHOD
com o valor GET
. O método to
recebe como parâmetro "http://{{authors.url}}/authors"
, ao usar duplas chaves envolta do authors.url
o camel é capaz de substituir o valor direto do application.properties
, dessa forma, o arquivo deve conter o valor da url:
#application.properties
authors.url=localhost:8081
Isso é tudo que precisamos para essa rota, o retorno do endpoint da API de autores é retornado diretamente pela aplicação Camel.
Agregação das consultas de autor e livros
A rota direct:call-rest-author
busca o autor pelo nome informado, com base na resposta recuperamos o ìd
do autor e em seguida enriquecemos o retorno com os livros do autor, conforme a imagem:
O código da rota fica da seguinte maneira:
from("direct:call-rest-author")
.routeId("call-rest-services")
.to("direct:author-service")
.process(new GetAuthorIdProcessor())
.enrich("direct:books-service", new JsonRestCallsAggregator())
Vamos falar das partes separadamente:
-
to("direct:author-service")
: Assim como a rota que retorna todos os autores, é feita uma chamada simples a API de autores:
from("direct:author-service")
.routeId("author-service")
.removeHeaders("CamelHttp*")
.setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{authors.url}}/authors/${header.name}");
Ao chamarmos o endpoint /integration/authors/{name}
da nossa aplicação camel, o path que for passado em name
fica disponível no header da exchange (ex: /integration/authors/jr-tolkien
; /integration/authors/jane-austin
), como o path faz a url de consulta a API ter diversas variações temos que usar o método toD
ao invés de to
.
O retorno da API de autor é um json com as informações do autor, para recuperarmos o id
precisamos fazer um parse do json e o mesmo se encontra encapsulado dentro da mensagem, que o Camel utiliza para transitar as informações entre as integrações, a Exchange, para o componente http
o response encontra-se no corpo da parte In da Exchange, conforme a imagem. Mais detalhes sobre a Exchange podem ser encontrados no javadoc https://www.javadoc.io/doc/org.apache.camel/camel-api/latest/org/apache/camel/Exchange.html.
-
process(new GetAuthorIdProcessor())
: Para recuperarmos oid
vamos usar um processor para ler a exchange, a classeGetAuthorIdProcessor
que implementaorg.apache.camel.Processor
contem o código necessário.
Classe GetAuthorIdProcessor.java
public class GetAuthorIdProcessor implements Processor {
@Override
public void process(Exchange exchange) throws Exception {
String author = exchange.getIn().getBody(String.class);
JsonParser parser = JsonParserFactory.getJsonParser();
Map<String, Object> jsonMap = parser.parseMap(author);
String authorId = (String) jsonMap.get("id");
exchange.getIn().setHeader("id", authorId);
exchange.getIn().setBody(author);
}
}
O método process é responsável por ler a resposta da API de autores que se encontra no corpo da parte In da Exchange. Como a resposta está em json usaremos classes do próprio Spring Boot para fazer o parse e ler o id. Por fim criamos um novo header chamado id
e setamos com o valor do id
do autor e setamos novamente o corpo da mensagen In com o json de autor.
-
enrich("direct:books-service", new JsonRestCallsAggregator())
: Nesse trecho é feita uma chamada para a API de livros e com o retorno realizamos o agrupamento das mensagens na classeJsonRestCallsAggregator
.
from("direct:books-service")
.routeId("books-service")
.removeHeaders("CamelHttp*")
.setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{books.url}}/books/${header.id}");
Assim como a chamada direct:author-service
usamos o componente http
para chamar a API de livros, como o path exige um id
passamos o que foi extraido do autor e inserido no header no método process(new GetAuthorIdProcessor())
, conforme a imagem abaixo. O valor {{books.url}}
deve ser declarado no application.properties
#application.properties
authors.url=localhost:8081
books.url=localhost:8082
A classe que agrega as respostas é uma implementação de org.apache.camel.AggregationStrategy
, com o código:
public class JsonRestCallsAggregator implements AggregationStrategy {
@Override
public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
JsonParser parser = JsonParserFactory.getJsonParser();
String books = newExchange.getIn().getBody(String.class);
String author = oldExchange.getIn().getBody(String.class);
JsonObject authorJson = new JsonObject(parser.parseMap(author));
authorJson.put("books", new JsonArray(parser.parseList(books)));
newExchange.getIn().setBody(authorJson.toJson());
return newExchange;
}
}
O método aggregate
que implementamos possui dois parâmetros, a Exchange antes do enrich
e depois dele. Com auxílio das classes do Spring Boot para manipular json recuperamos o json de autor do oldExchange
e json dos livros do autor do newExchange
, conforme a imagem:
Dessa forma criamos um Exchange de retorno contendo um json no formato:
{
"author": {
"id": "...",
"name": "...",
"fullName": "...",
"books": [ ... ]
}
}
Lidando com erros
Com nossa aplicação Camel funcionando podemos consumir de forma integrada os serviços das duas APIs mas e se houver alguma falha na chamada delas?
Vamos cobrir dois casos e refatorar nossa API para ser mais tolerante a falhas.
Cenários de falha
- Falha na API de livros.
Caso seja utilizado um id de autor que não possua livros cadastrados durante a chamada da API de livros (
/books/{authorId}
) teremos como retorno o erro 404 (Not Found). Para evitar erros na nossa aplicação Camel vamos tratar a chamada ao endpoint. O componentehttp
já possui um comportamento em que dependendo do código de status do response o componente retorna sucesso ou a exceçãoHttpOperationFailedException
[1].
//Sem tratamento de erro
from("direct:books-service")
.routeId("books-service")
.removeHeaders("CamelHttp*")
.setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{books.url}}/books/${header.id}");
//Com tratamento de erro
from("direct:books-service")
.routeId("books-service")
//tratamento de exceção
.onException(HttpOperationFailedException.class)
.handled(true)
.setBody(constant("[]"))
.end()
.removeHeaders("CamelHttp*")
.setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{books.url}}/books/${header.id}");
Com a modificação, toda vez que tivermos o erro 404 da API de livros a aplicação Camel irá utilizar um array vazio []
pra completar o request.
- Falha na API de autores.
Caso seja utilizado um nome que não exista durante a chamada da API de autores (
/authors/{name}
) a API irá retornar o código de status204
(No Content). Para evitar erros na nossa aplicação Camel vamos tratar a chamada ao endpoint. Como o status204
encontra-se na família de status sucesso podemos checar o status de retorno da API de autores e caso seja204
iremos retornar o mesmo código pela aplicação Camel, simbolizando que o nome do autor não foi encontrado. Da mesma forma podemos retornar404
ou outro código, com um simples alteração no código.
//Sem tratamento de erro
from("direct:call-rest-author")
.routeId("call-rest-services")
.to("direct:author-service")
.bean("authors", "getId")
.enrich("direct:books-service", new JsonRestCallsAggregator())
//variáveis
private static final int OK_CODE = 200;
private static final int APP_RESPONSE_CODE = 204;
//Com tratamento de erro
from("direct:call-rest-author")
.routeId("call-rest-services")
.to("direct:author-service")
.choice()
.when(header(Exchange.HTTP_RESPONSE_CODE).isEqualTo(OK_CODE))
.bean("authors", "getId")
.enrich("direct:books-service", new JsonRestCallsAggregator())
.otherwise()
.setHeader(Exchange.HTTP_RESPONSE_CODE).constant(APP_RESPONSE_CODE);
Com a modificação, toda vez que tivermos o erro 204
da API de autores a aplicação Camel irá retornar o erro definido na variável APP_RESPONSE_CODE
encerrando a requisição, evitando a consulta a API de livros e uma possível exceção.
Conclusão
O Apache Camel é um framework bastante robusto para construir diversos tipos de integrações como a que foi apresentada nesse artigo. O EIP Enricher assim como outros EIPs implementados no Camel são muito poderosos, para mais detalhes sobre cada um deles é possível consultar a documentação em https://camel.apache.org/components/latest/eips/enterprise-integration-patterns.html.
A aplicação Camel construída possui algumas possibilidades de expansão como a adição de integração com novos endpoints ou APIs, a melhoria no tratamento de erro (para verificar outras exceções e falhas), a chamada inversa ao buscar um livro e enriquecer com os informações do autor.
Posted on July 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.