Utilizando Apachel Camel para agregar endpoints de REST APIs

thegusmao

André Luiz de Gusmão

Posted on July 27, 2020

Utilizando Apachel Camel para agregar endpoints de REST APIs

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.

Camel

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:

  1. API de autores de livros com os endpoints:
    1. [GET] /authors
    2. [GET] /authors/{name}
  2. API de livros com o endpoint:
    1. [GET] /books/{authorId}

Nossa aplicação Camel vai disponibilizar dois endpoints:

  1. [GET] /integration/authors - Consulta a API de autores.
  2. [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'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Route 1

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())
Enter fullscreen mode Exit fullscreen mode

Vamos falar das partes separadamente:

  1. 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}");
Enter fullscreen mode Exit fullscreen mode

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.

Route 2

  1. process(new GetAuthorIdProcessor()): Para recuperarmos o id vamos usar um processor para ler a exchange, a classe GetAuthorIdProcessor que implementa org.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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 classe JsonRestCallsAggregator.
from("direct:books-service")
  .routeId("books-service")
  .removeHeaders("CamelHttp*")
  .setHeader(Exchange.HTTP_METHOD, constant("GET"))
.toD("http://{{books.url}}/books/${header.id}");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Route 3

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Route 4

Dessa forma criamos um Exchange de retorno contendo um json no formato:

{
    "author": { 
        "id": "...",
        "name": "...",
        "fullName": "...",
        "books": [ ... ]
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. 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 componente http já possui um comportamento em que dependendo do código de status do response o componente retorna sucesso ou a exceção HttpOperationFailedException [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}");
Enter fullscreen mode Exit fullscreen mode
//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}");   
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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 status 204 (No Content). Para evitar erros na nossa aplicação Camel vamos tratar a chamada ao endpoint. Como o status 204 encontra-se na família de status sucesso podemos checar o status de retorno da API de autores e caso seja 204 iremos retornar o mesmo código pela aplicação Camel, simbolizando que o nome do autor não foi encontrado. Da mesma forma podemos retornar 404 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())
Enter fullscreen mode Exit fullscreen mode
//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);
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
thegusmao
André Luiz de Gusmão

Posted on July 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related