DEV Community

loading...
Cover image for Utilizando Apachel Camel para agregar endpoints de REST APIs

Utilizando Apachel Camel para agregar endpoints de REST APIs

andredgusmao profile image André Luiz de Gusmão Originally published at dev.to ・8 min read

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'

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:

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())

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}");

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);
    }
}

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}");

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

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;
    }
}

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": [ ... ]
    }
}

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}");
//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.

  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())
//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.

Discussion (0)

pic
Editor guide