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.
Top comments (0)