APIs e Microsserviços

14 ago, 2017

Como construir uma API Rest com Reactive Spring and Spring Boot 2.0

Publicidade

Em poucas palavras, programação reativa são aplicações não-blocantes, orientadas por eventos que se dimensionam com um pequeno número de threads com contrapressão como ingrediente-chave, que visa garantir que os produtores não dominem os consumidores. A especificação dos fluxos reativos (também adotada em Java 9) permite a capacidade de comunicar a demanda em camadas e bibliotecas de diferentes provedores. Por exemplo, uma conexão HTTP que escreve para um cliente pode comunicar sua disponibilidade para escrever todo o caminho a montante para um repositório em um banco de dados, de modo que em um cliente HTTP lento, o repositório pode diminuir a velocidade ou mesmo pausar. Para uma introdução mais extensa à programação reativa, verifique a série de artigos de Dave Syer – Notes on Reactive Programming Pat I: The Reactive Landscape.

A maioria dos aplicativos da Web Java são criados na API de Servlet, que foi originalmente criada com semântica síncrona e de bloqueio. Ao longo dos anos, foi adicionado suporte para solicitações assíncronas (Servlet 3.0) e I/O sem bloqueio (Servlet 3.1). No Spring MVC foi identificado que é viável adicionar manipulação de solicitação HTTP assíncrona seletiva para aplicativo. No entanto, também descobriram que é muito difícil introduzir I/O não-blocantes dentro de um ecossistema existente de frameworks e aplicativos da Web. Isso exige mudanças muito profundas até nos contratos principais, que precisam alternar do bloqueio para a semântica assíncrona.

Uma das razões para a popularidade contínua do Spring MVC é seu modelo de programação intuitivo, baseado em anotações e em assinaturas de métodos de controllers flexíveis. Felizmente, o mesmo pode continuar a servir de base para aplicativos web reativos. Esta é a direção do esforço Spring Reactive, na qual você vai encontrar um TestController que se parece com qualquer controlador Spring MVC, mas é executado em um novo mecanismo reativo com testes de integração podendo rodar no Jetty, Undertow e Netty.

O ingrediente chave para esse esforço é a especificação de fluxos reativos, cujo objetivo é fornecer um “padrão para processamento de fluxo assíncrono com processos não-blocantes”. A especificação permite a interoperabilidade entre os provedores de componentes assíncrono, desde os servidores HTTP até estruturas da Web, drivers de banco de dados etc. Ele será incluído no JDK 9 como java.util.concurrent.Flow.

A especificação é pequena e consiste em quatro interfaces, algumas regras e um TCK (Technology Compatibility Kit) e para expor isso como uma API, no entanto, é necessária uma infraestrutura em torno dele para compor uma lógica assíncrona. O Spring Reactive usa o Reactor Core, uma pequena biblioteca que serve de base para os frameworks que desejam construir Streams Reativos.

Sem mais delongas, vamos ao código!

No exemplo de hoje, vamos criar uma pequena API utilizando Spring Boot e um banco de dados MongoDB. Caso precise de ajuda para instalação do Mongo, dê uma olhadinha nesse artigo que tem alguns links úteis, ok?

Vamos até o site do Spring Initializr para criarmos nosso projeto com as devidas dependências. Caso queira, pode criar também dentro do STS ou do IntelliJ, sem problemas.

Defina o nome que achar melhor para o projeto, mas atente-se aos seguintes pontos que serão necessários:

  • Mude a versão do spring boot para 2.0.0.M2;
  • Selecione as dependências: Reactive Web, Reactive MongoDB e Lombok;
  • Faça o download do projeto e abra-o na sua IDE de preferência.

OBS: Por enquanto, o spring boot suporta programação reativa apenas para trabalhar com os banco de dados MongoDB, Cassandra e Redis e suas dependências estão disponíveis no Spring Initializr a partir da versão 2.0.0.SNAPSHOT com os nomes Reactive MongoDB, Reactive Cassandra e Reactive Redis.

Assim que o projeto estiver configurado em sua IDE com as dependências baixadas, vamos criar uma classe chamada Car (que será nosso modelo) com os atributos id e model e iniciar nosso desenvolvimento:

@Document
@Data
@AllArgsConstructor
@NoArgsConstructor
class Car{
 
  @Id
  private String id;
  private String model;
}

Como estamos utilizando banco de dados Mongo, devemos anotar nossa classe com @Document e as demais annotations que fazem parte do projeto Lombok (para saber um pouco mais sobre Lombok veja a documentação oficial no site: Project Lombok).

Para quem estiver utilizando IntelliJ, recomendo a instalação do plugin chamado “Lombok” para que a IDE não reclame quando criarmos um objeto que utiliza as anotações do Lombok.

Aproveitando, vamos criar também uma classe chamada CarEvents, que terá o papel de representar dados que serão apresentados no stream de eventos que vamos criar. Mais adiante, voltamos a esse assunto.

@Data
@AllArgsConstructor
@NoArgsConstructor
class CarEvents {
 
   private Car model;
   private Date when;
}

E agora um repositório para nossas operações no banco de dados:

interface MovieRepository extends ReactiveMongoRepository<Movie, String> { }

Note que nossa interface extends Reactive Mongo Repository está contida no módulo Reactive MongoDB que selecionamos no Spring Initializr. Esta interface já possui suporte para programação reativa.

Para fazer consulta em nossa entidade, nada mais justo que uma classe de serviço, que vou chamar de “FluxCarService”.

@Service
class FluxCarService {
 
   private final CarRepository carRepository;
 
   FluxCarService(CarRepository carRepository) {
       this.carRepository = carRepository;
   }
 
   public Flux<Car> all () {
       return carRepository.findAll();
   }
 
   public Mono<Car> byId(String carId) {
       return carRepository.findById(carId);
   }
 
   public Flux<CarEvents> streams(String carId) {
       return byId(carId).flatMapMany(car -> {
           Flux<Long> interval = Flux.interval(Duration.ofSeconds(1));
           Flux<CarEvents> events = Flux.fromStream(
                   Stream.generate(() -> new CarEvents(car, new Date())));
           return Flux.zip(interval, events).map(Tuple2::getT2);
       });
   }
}

Em nossa classe, temos um Flux (1:n), que será responsável por retornar todos os modelos de carros que temos no banco de dados Mono(1:1); que por sua vez, deverá retornar um modelo de carro a partir de um ID de identificação. Temos também um Flux de CarEvents, que será utilizado em nosso endpoint para criar um stream de eventos de um determinado dado. Note que ele espera receber um ID e, então, no intervalo de um segundo irá disparar um evento do conteúdo daquele dado com o respectivo ID. Em seguida, o método ZIP da classe Flux se encarrega de aguardar até que os dados (os eventos) estejam prontos e os envia em um response respeitando o intervalo definido.

Para fazer o input dos dados no banco, vamos criar um componente com algumas informações para serem salvas. Mas para isso, faremos uso da classe Flux do Project Reactor para iniciarmos nosso processo, “nosso fluxo”. Você vai notar que nossa classe seguirá uma cadeia de chamadas (um fluxo) para realizar o input de nossos dados no banco. A ideia do reactive programming é que tudo seja baseado em eventos. E quem executa esses eventos? O framework! De fato, ele vai saber a melhor hora para disparar uma chamada de forma a otimizar nossos recursos.

Como gosto de carros, vou utilizar alguns modelos em nosso exemplo. Note o fluxo de ações que será executado ao iniciar nosso sistema.

@Component
class DummyData implements CommandLineRunner {
 
  private final CarRepository carRepository;
 
  DummyData(CarRepository carRepository) {
     this.carRepository = carRepository;
  }
 
  @Override
  public void run(String... args) throws Exception {
     carRepository.deleteAll()
           .thenMany(
                 Flux.just("Koenigsegg One:1", "Hennessy Venom GT", "Bugatti Veyron Super Sport",  "SSC Ultimate Aero", "McLaren F1", "Pagani Huayra", "Noble M600",
                       "Aston Martin One-77", "Ferrari LaFerrari", "Lamborghini Aventador")
                 .map(model -> new Car(UUID.randomUUID().toString(), model))
                 .flatMap(carRepository::save))
           .subscribe(System.out::println);
  }
}

Para não encher de informações nosso banco de dados, sempre que a aplicação for reiniciada, o primeiro passo do fluxo é excluir os dados que estiverem no banco e, assim que essa etapa estiver concluída, ele avança inserindo e exibindo no console nossas informações.

Agora vamos criar alguns endpoins para realizar a consulta dos dados que acabamos de inserir. E para deixar bem visível a diferença entre a escrita de um código orientado a eventos (fluxos) e a forma pela qual escrevemos métodos atualmente, vamos primeiro ao desenvolvimento tradicional e depois vamos reescrevê-los utilizando a API de Streams do Project Reactor, implementada pelo Spring Boot.

Tradicional:

@RestController
class CarController {
 
   private final FluxCarService fluxCarService;
 
   CarController(FluxCarService fluxCarService) {
       this.fluxCarService = fluxCarService;
   }
 
   @GetMapping("/cars")
   public Flux<Car> all() {
       return fluxCarService.all();
   }
 
   @GetMapping("/cars/{carId}")
   public Mono<Car> byId(@PathVariable String carId) {
       return fluxCarService.byId(carId);
   }
 
   @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE, value = "/cars/{carId}/events")
   public Flux<CarEvents> eventsOfStreams(@PathVariable String carId) {
       return fluxCarService.streams(carId);
   }
}

Bem trivial, certo? Então, bora para os testes! Vamos garantir que nossos endpoins estão funcionando corretamente e no terminal vamos fazer algumas chamadas para validar nosso trabalho. Vou utilizar o famoso “CURL”.

E o resultado da nossa primeira chamada para o endpoint “/cars” é:

➜  car-models-reactive curl http://localhost:8080/cars | json_pp
 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   737    0   737    0     0  45278      0 --:--:-- --:--:-- --:--:-- 46062
[
   {
      "id" : "3fb05505-7dcf-4c85-bb77-4977e581efed",
      "model" : "Koenigsegg One:1"
   },
   {
      "model" : "SSC Ultimate Aero",
      "id" : "516ed32f-aef9-4826-b51d-c95bf6e9d09b"
   },
   {
      "model" : "Hennessy Venom GT",
      "id" : "cc97348a-4b1d-450d-b945-ae93bf93320d"
   },
   {
      "model" : "McLaren F1",
      "id" : "9b98b4cf-0894-4e9e-8890-a2b9a6be0f77"
   },
   {
      "model" : "Bugatti Veyron Super Sport",
      "id" : "43ab3769-7f90-409f-af23-a66acfe6ea6b"
   },
   {
      "id" : "ef872ab2-373f-4973-b039-46dbf98a68b4",
      "model" : "Pagani Huayra"
   },
   {
      "model" : "Noble M600",
      "id" : "ac9f8d71-9604-42af-9827-cc02486d8f60"
   },
   {
      "id" : "e762712d-28ea-4222-84c5-8a470e6402d8",
      "model" : "Aston Martin One-77"
   },
   {
      "model" : "Ferrari LaFerrari",
      "id" : "fc01c8a8-7b39-4ef3-9e3c-59a3d0152593"
   },
   {
      "model" : "Lamborghini Aventador",
      "id" : "c37a4128-405f-4550-adcd-6a5b420938bb"
   }
]

Muito bem! Menos um. Agora, vamos realizar uma busca pelo ID. Copie qualquer ID do resultado da chamada acima para utilizarmos no próximo teste:

➜  car-models-reactive curl http://localhost:8080/cars/fc01c8a8-7b39-4ef3-9e3c-59a3d0152593 | json_pp
 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    73    0    73    0     0   6597      0 --:--:-- --:--:-- --:--:--  7300
{
   "model" : "Ferrari LaFerrari",
   "id" : "fc01c8a8-7b39-4ef3-9e3c-59a3d0152593"
}

Checked! E finalmente nosso stream de eventos… Vou utilizar na chamada o mesmo ID no exemplo anterior. Para chamar nosso endpoint, repita a URI acima e coloque no final/events:

➜  car-models-reactive curl http://localhost:8080/cars/fc01c8a8-7b39-4ef3-9e3c-59a3d0152593/events   
 
data:{"model":{"id":"fc01c8a8-7b39-4ef3-9e3c-59a3d0152593","model":"Ferrari LaFerrari"},"when":1500031877153}
 
data:{"model":{"id":"fc01c8a8-7b39-4ef3-9e3c-59a3d0152593","model":"Ferrari LaFerrari"},"when":1500031878155}
 
data:{"model":{"id":"fc01c8a8-7b39-4ef3-9e3c-59a3d0152593","model":"Ferrari LaFerrari"},"when":1500031879155}
 
data:{"model":{"id":"fc01c8a8-7b39-4ef3-9e3c-59a3d0152593","model":"Ferrari LaFerrari"},"when":1500031880155}
 
data:{"model":{"id":"fc01c8a8-7b39-4ef3-9e3c-59a3d0152593","model":"Ferrari LaFerrari"},"when":150003188

Muito bem. Temos, então, nosso event streams que é disparado a cada 1 segundo. Legal! Agora vamos voltar para o assunto deste artigo. Vamos programar utilizando recursos reativos, os nossos endpoints, para que eles trabalhem de forma assíncrona e o framework faça o gerenciamento das chamadas de forma que otimize os recursos do nosso “servidor” e garanta a melhor performance.

Informação importante: o SpringBoot não está utilizando o TomCat como container para nossa aplicação. Isso mesmo, por enquanto o TomCat não oferece suporte para programação assíncrona reativa no SpringBoot. A partir da versão 2.0.0 o container (web server) de aplicação default utilizado é o Netty.

(saida do console no IntelliJ)
2017-07-14 08:18:33.364  INFO 5080 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080

“Netty is an asynchronous event-driven network application framework  for rapid development of maintainable high performance protocol servers & clients.”

Até há alguns meses atrás para conseguirmos testar reactive programming no Spring Boot precisávamos configurar um HttpServer (do projeto netty reactor) na “unha”. O código era esse:

@Bean
HttpServer server(RouterFunction<?> router) {
  HttpHandler handler = RouterFunctions.toHttpHandler(router);
  HttpServer httpServer = HttpServer.create(8000);
  httpServer.start(new ReactorHttpHandlerAdapter(handler));
  return httpServer;
}

Mas aí, veio o Spring Boot Team e facilita mais essa. Thanks, Spring Boot team!

Voltando aos nossos endpoins reativos… Vamos criar um @component dentro da nossa classe principal do projeto; a mesma que tem o método Main, ok?! Este componente terá um papel muito importante: ele será nosso “identificador de rotas” -> Route Handles.

Não se esqueça de comentar a annotation da nossa classe controller @RestController para não termos problemas.

@Component
public static class RouteHandles {
   private final FluxCarService fluxCarService;
 
   public RouteHandles(FluxCarService fluxCarService) {
       this.fluxCarService = fluxCarService;
   }
 
   public Mono<ServerResponse> allCars(ServerRequest serverRequest) {
       return ServerResponse.ok()
               .body(fluxCarService.all(), Car.class)
               .doOnError(throwable -> new IllegalStateException("My godness NOOOOO!!"));
   }
 
   public Mono<ServerResponse> carById(ServerRequest serverRequest) {
       String carId = serverRequest.pathVariable("carId");
       return ServerResponse.ok()
               .body(fluxCarService.byId(carId), Car.class)
               .doOnError(throwable -> new IllegalStateException("oh boy... not againnn =(("));
   }
 
   public Mono<ServerResponse> events(ServerRequest serverRequest) {
       String carId = serverRequest.pathVariable("carId");
       return ServerResponse.ok()
               .contentType(MediaType.TEXT_EVENT_STREAM)
               .body(fluxCarService.streams(carId), CarEvents.class)
               .doOnError(throwable -> new IllegalStateException("I give up!! "));
   }
}

Com nossas Router Handles prontas, podemos escrever um @Bean Router Functions que vai usar e abusar delas:

@Bean
RouterFunction<?> routes(RouteHandles routeHandles) {
   return RouterFunctions.route(
           RequestPredicates.GET("/cars"), routeHandles::allCars)
           .andRoute(RequestPredicates.GET("/cars/{carId}"), routeHandles::carById)
           .andRoute(RequestPredicates.GET("/cars/{carId}/events"), routeHandles::events);
}

E seguem nossas funções, nossas rotas, nossos endpoints reativos e funcionais. Essa Router Function é equivalente ao @Controller que tínhamos escrito anteriormente.

Obs: O IntelliJ já possui um plugin para nos ajudar quando for necessário debugar streams, ok? O nome dele é Java Stream Debugger. E bora testar novamente. A URI das nossas rotas (“endpoints”) continuam as mesmas. Sempre que um request for feito, um interceptor vai capturar a chamada e encaminhar para a rota correspondente para completar:

➜  / curl http://localhost:8080/cars | json_pp           
                          
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   737    0   737    0     0  43491      0 --:--:-- --:--:-- --:--:-- 46062
[
   {
      "model" : "Koenigsegg One:1",
      "id" : "ab337264-a905-4950-b64e-2936ad7d4c46"
   },
   {
      "id" : "68252e12-427d-4c8a-9d5f-654047b2e5c4",
      "model" : "Bugatti Veyron Super Sport"
   },
   {
      "model" : "SSC Ultimate Aero",
      "id" : "0f84ab62-3a5f-4b1e-a2bf-7bd603fb4c47"
   },
   {
      "model" : "Hennessy Venom GT",
      "id" : "d0eab81f-cb1d-44ce-a91c-9193b207be13"
   },
   {
      "id" : "5c6b18d8-f316-4fee-afab-7d7cd0073b38",
      "model" : "McLaren F1"
   },
   {
      "id" : "62d1b8aa-d76e-4ba5-9e4f-60adea6fe20d",
      "model" : "Pagani Huayra"
   },
   {
      "id" : "dc3d8690-fd37-4da6-a10e-1f6b0dd73561",
      "model" : "Noble M600"
   },
   {
      "model" : "Aston Martin One-77",
      "id" : "9042ec0c-4b3b-4e5d-8c25-358c51efc4c3"
   },
   {
      "model" : "Ferrari LaFerrari",
      "id" : "8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e"
   },
   {
      "id" : "be70d689-ec2c-4761-81b1-b0d7755d6ab9",
      "model" : "Lamborghini Aventador"
   }
]
 
➜  / curl http://localhost:8080/cars/8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e | json_pp
 
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    73    0    73    0     0   4498      0 --:--:-- --:--:-- --:--:--  4562
{
   "model" : "Ferrari LaFerrari",
   "id" : "8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e"
}
 
➜  / curl http://localhost:8080/cars/8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e/events   
 
data:{"model":{"id":"8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e","model":"Ferrari LaFerrari"},"when":1500036022786}
 
data:{"model":{"id":"8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e","model":"Ferrari LaFerrari"},"when":1500036023788}
 
data:{"model":{"id":"8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e","model":"Ferrari LaFerrari"},"when":1500036024788}
 
data:{"model":{"id":"8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e","model":"Ferrari LaFerrari"},"when":1500036025787}
 
data:{"model":{"id":"8d1e4f5f-e2c3-4d2e-8f73-2febb16cb23e","model":"Ferrari LaFerrari"},"when":1500036026788}
E

E os ganhos? Podem variar dependendo do equipamento que está sendo utilizado para testes e também dos recursos utilizados. Mas nos testes que realizei com esses poucos registros, de cara temos um ganho na performance até o response da chamada. Provavelmente, esse ganho seja mais visível em um sistema maior e com muito mais dados. Note o tempo do Avarage.

(/cars)

curl http://localhost:8080/cars | json_pp (trivial endpoints)

curl http://localhost:8080/cars | json_pp (reactive programming)

(/cars/{id})

curl http://localhost:8080/cars/fc01c8a8-7b39-4ef3-9e3c-59a3d0152593 (trivial endpoints)

curl http://localhost:8080/cars/fc01c8a8-7b39-4ef3-9e3c-59a3d0152593 (reactive programming)

O que acharam? Usar? Não usar? Depende, né? Agora basta esperharmos pela versão 2.0.0.RELEASE e sair codando. Voce pode baixar essa API no github. Tem alguma dúvida ou algo a dizer? Aproveite os campos abaixo.

***

Artigo originalmente publicado em: https://www.concrete.com.br/2017/07/28/reactive-spring-construindo-uma-api-rest-com-reactive-spring-and-spring-boot-2-0/