Por: @Caiuzu
ESTUDO JAVA: Solução E-commerce.
Neste projeto prático iremos desenvolver uma solução de e-commerce com a arquitetura de microservices e aplicar a integração entre eles orientada a eventos com Apache Kafka e garantir a compatibilidade entre da comunicação dos microservices com Schema Registry. Para isso, programaremos em Java utilizando a stack do Spring (Spring Boot, Spring Cloud Streams).
#Microsservice #Spring #Kafka #Avro #SchemaRegistry
Primeiros passos para desenvolver um Projeto:
-
Instalação Windows:
- Primeiramente baixar e instalar https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
- Após o término da instalação vamos configurar as variáveis do ambiente:
- Clique com o botão direito em cima do ícone “Meu Computador”;
- Na barra de busca procure por “Variáveis de ambiente” e acesse "Editar Variáveis de Ambiente do Sistema";
- Clique no botão “Novo…” em “Variáveis de Ambiente”;
- Nome da variável:
JAVA_HOME
- Valor da variável: coloque aqui o endereço de instalação (o caminho tem que ser o de instalação)
C:\Arquivos de programas\Java\jdk1.8.0
- Clique em OK
- Nome da variável:
- Clique novamente no botão
Nova
em Variáveis do sistema;- Nome da variável: CLASSPATH
- Os valores da variável encontram-se abaixo, sempre insira um do lado outro sem espaços e com o
;
(ponto e vírgula) no final.
;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;%JAVA_HOME%\lib\dt.jar; %JAVA_HOME%\lib\htmlconverter.jar;%JAVA_HOME%\jre\lib;%JAVA_HOME%\jre\lib\rt.jar;
- Selecione a variável PATH em Variáveis do sistema e clique no botão Editar.
- Defina o valor dessa variável com o caminho da pasta Bin. No caso, pode-se utilizar a variável
JAVA_HOME
previamente definida.
;%JAVA_HOME%\bin
- Defina o valor dessa variável com o caminho da pasta Bin. No caso, pode-se utilizar a variável
- Por fim, vamos verificar com
javac -version
ejava -version
- Baixe: https://maven.apache.org/download.cgi
- apache-maven-3.8.1-bin.zip
- Extrair os arquivos, por exemplo, no diretório, raiz:
C:
- Adicionar nas variáveis de ambiente, em PATH com a localização do bin, por exemplo:
C:\Program Files\apache-maven-3.8.1\bin
- Em seguida vamos testar no terminal com o comando:
gradle -v
- Baixe: https://gradle.org/releases/
- Extrair os arquivos, por exemplo, no diretório root
C:
- Adicionar nas variáveis de ambiente, em PATH com a localização do bin, por exemplo:
C:\Program Files\gradle-7.0\bin
- Em seguida vamos testar no terminal com o comando:
gradle -v
-
Instalação Linux — SDKMAN:
-
Install SDK Man:
apt-get install curl curl -s "https://get.sdkman.io" | bash source "/home/user/.sdkman/bin/sdkman-init.sh"
-
Install Java 8:
sdk install java 8u272-albba sdk list java
-
Install maven:
sdk install maven
-
Install gradle:
sdk install gradle
-
-
Primeiro, atualize sua lista existente de pacotes:
sudo apt update
-
Em seguida, instale alguns pacotes de pré-requisitos que permitem ao apt usar pacotes sobre HTTPS:
sudo apt install apt-transport-https ca-certificates curl software-properties-common
-
Em seguida, adicione a chave GPG para o repositório oficial do Docker ao seu sistema:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
-
Adicione o repositório Docker às fontes APT:
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
-
Em seguida, atualize o banco de dados de pacotes com os pacotes Docker do repo recém-adicionado:
sudo apt update
-
Certifique-se de que está prestes a instalar a partir do repositório Docker em vez do repositório Ubuntu padrão:
apt-cache policy docker-ce
Observe que docker-ce não está instalado, mas o candidato para instalação é do repositório Docker para Ubuntu 20.04 (focal). Finalmente, instale o Docker:
sudo apt install docker-ce
-
O Docker agora deve estar instalado, o daemon iniciado e o processo habilitado para iniciar na inicialização. Verifique se ele está funcionando:
sudo systemctl status docker #para linux sudo /etc/init.d/docker status #para wsl2
-
Se quiser evitar digitar sudo sempre que executar o comando docker, adicione seu nome de usuário ao grupo docker:
sudo usermod -aG docker ${USER}
-
Para aplicar a nova associação de grupo, saia do servidor e entre novamente ou digite o seguinte:
#Você será solicitado a inserir sua senha de usuário para continuar. su - ${USER}
-
Confirme se o seu usuário foi adicionado ao grupo docker digitando:
id -nG
#Output sammy sudo docker
-
listar docker:
docker ps
run test docker
docker run hello-world
-
Instale também o docker compose (utilizaremos 1.28.2)
sudo curl -L "https://github.com/docker/compose/releases/download/1.28.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
-
dando permissão de execução para docker-compose:
sudo chmod +x /usr/local/bin/docker-compose
-
Verificando versão:
docker-compose --version
Iremos instalar o portainer.io para termos uma visualização dos containers:
- Criando volume:
docker volume create portainer_data
- Instalando portainer no volume:
docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce
- Para acessar: http://localhost:9000/
Ao utilizarmos a docker, podemos nos deparar com a impossibilidade de conectarmos aos serviços de forma externa (pelo navegador)
-
Para verificar o acesso às portas:
telnet localhost 9000 netcat -l 127.0.0.1 9000 ps aux | grep http
-
Para solucionar basta:
- Desabilitar o Fast Boot do Windows;
- Executar o comando no powershell:
wsl --shutdown
As ilustrações abaixo foram criadas com asciiflow.
Breve entendimento: O modelo de negócios desse projeto, o usuário vai passar pela tela onde escolhe os produtos, monta o carrinho. Na hora de fazer o checkout, ele simplesmente informa os dados e na hora que efetua a compra, o pedido não será processado no mesmo momento em que foi feito, aparecerá uma tela de "estamos processando seu pedido", assim como fazem a maioria dos e-commerces. Reservam o saldo no cartão e faz o processamento depois. Capturaremos da reserva de saldo. Não iremos processar nada em checkout-api. No momento em que seja feita a cobrança, manda uma notificação que a compra está aprovada. iremos salvar nosso checkout e então faremos com que a payment-api processe efetivamente o pagamento com os dados enviados pelo checkout.
-
Domínios:
┌────────────────────────────────┐ │ E-COMMERCE │ │ ┌──────────┐ ┌───────────┐ │ │ │ CHECKOUT │ │ PAYMENT │ │ │ └──────────┘ └───────────┘ │ └────────────────────────────────┘
[checkout]
: guarda as informações de checkout (cartão de crédito/débito, dados de usuários).[payment]
: possuí a responsabilidade de cobrar do cartão com o valor de uma compra "XPTO".
-
Arquitetura:
-
[Checkout]
:- checkout-front: Uma tela simples, com um botão comprar
- checkout-api: Ao receber o evento de comprar enviará um evento ao kafka
-
[Payment]
:- payment-api: Ao receber o evento do kafka faz o pagamento e devolve outro evento ao kafka.
-
Explicação: abaixo, checkout-api, irá gerar um evento para o kafka, onde payment-api estará escutando. Ao finaliza o processamento de pagamento, payment-api irá retornar outro evento para o kafka onde checkout-api, irá escutar.
┌──────────────┐ ┌────────────┐ ┌─────┐ ┌───────────┐ │checkout-front├──►│checkout-api├──►│kafka├──►│payment-api│ └──────────────┘ └───────────▲┘ └┬───▲┘ └┬──────────┘ └─────┘ └─────┘
-
-
Apache Kafka;
- Primeiramente, temos que entender oque é Streaming Data: É basicamente um fluxo contínuo de dados, como um rio
-
Para quê e por quê utiliza: possuí a capacidade de coletar, processar e armazenar um grande volume de dados em tempo real. Alta disponibilidade dos dados e confiabilidade.
┌────────────────────────────────────────────────────────────────┐ │ ┌────────────┐ ┌──────────────┐ │ │ │ Produtor 1 ├─────┐ ┌────►│ Consumidor 1 │ │ │ └────────────┘ │ │ └──────────────┘ │ │ │ │ │ │ ┌────────────┐ │ ┌────────┐ │ ┌──────────────┐ │ │ │ Produtor 2 ├─────┼───►│ Broker ├───┼────►│ Consumidor 2 │ │ │ └────────────┘ │ └────────┘ │ └──────────────┘ │ │ │ │ │ │ ┌────────────┐ │ │ ┌──────────────┐ │ │ │ Produtor N ├─────┘ └────►│ Consumidor N │ │ │ └────────────┘ └──────────────┘ │ └────────────────────────────────────────────────────────────────┘ Os produtores irão produzir os informações para um broker e disponibilizá-los para os consumidores.
-
- O Apache Kafka, é uma plataforma distribuída de mensagens e streaming.
Diferente de Redis, rabbitMQ, que são sistemas de mensagerias.
-
A ideia do streaming no kafka é o mesmo de um broadcast com TCP em redes, ele replica para outros IPs, mas só quem está pronto para recebe-lo irá consumir o dado.
┌───────────────────────────────────────────────────────────────┐ │ Producers │ │ ┌─────┐ │ │ │ APP ├───┐ │ │ └─────┘ │ ┌─────┐ │ │ │ ┌──►│ APP │ │ │ ┌────┐ │ │ └─────┘ │ │ │ DB ├──┐ ▼ │ │ │ └────┘ │ ┌────────┐◄─┘ Stream │ │ Connector ├──►│ Broker │ Processors │ │ ┌────┐ │ └────────┘◄─┐ │ │ │ DB ├──┘ ▲ │ │ │ └────┘ │ │ ┌─────┐ │ │ │ └──►│ APP │ │ │ ┌─────┐ │ └─────┘ │ │ │ APP │◄──┘ │ │ └─────┘ │ │ Consumers │ └───────────────────────────────────────────────────────────────┘
-
- Primeiramente, temos que entender oque é Streaming Data: É basicamente um fluxo contínuo de dados, como um rio
-
Conceitos:
-
Connectors
: conseguimos conectar o banco e disparar, por exemplo, um evento sempre que for feito um insert. -
Mensagens
: A informação produzida pelo produtor -
Tópicos
: Meio por onde o produtor vai postar a mensagem e o consumidor consumirá- Pode ser formado por N partições. Quando um produtor, publica uma mensagem, vai para uma dada partição;
- Cada partição possuí uma ordem de mensagens, de forma que conseguimos garantir a ordem das partições, mas não dos itens dentro destas.
Partição 1 -> [1] [2] [3] [4] [5] [6] [7] Partição 2 -> [1] [2] [3] [4] [5] Partição 3 -> [1] [2] [3] [4] [5] [6]
- Cada
offset
, como é chamada cada posição dentro da partição, em determinado ciclo de vida da aplicação, será lido por mais de um consumidor, mesmo que este não seja quem irá consumi-lo. Lembrando que todas as mensagens produzidas podem ser escutadas por todos os consumidores.
- Cada
- Cada partição possuí uma ordem de mensagens, de forma que conseguimos garantir a ordem das partições, mas não dos itens dentro destas.
- Pode ser formado por N partições. Quando um produtor, publica uma mensagem, vai para uma dada partição;
-
Produtores
: quem produz a mensagem; -
Consumidores
: quem consome a mensagem; -
Brokers
: As instâncias do Kafka; -
Clusters
: Conjuntos de Brokers;- Quando formamos um Cluster Kafka, conseguimos criar mais de um servidor e conectar a escuta em um grupo de consumidores específicos para cada servidor.
-
Apache Zookeeper
: Gerenciador de Clusters;- Abaixo temos uma formação de um cluster com 4 brokers, para administrar estes, utiliza-se o Zookeeper.
- Uma exemplo de organização possível, é a nomeação automática de quem será master/slave.
- O Kafka utiliza o Zookeeper para sincronizar as configurações entre diferentes clusters.
Cluster ┌─────────────────────────────────────────────────────────────┐ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Broker 1 │ │ Broker 2 │ │ Broker 3 │ │ Broker 4 │ │ │ └────┬─────┘ └───┬──────┘ └─────┬────┘ └────┬─────┘ │ │ │ │ │ │ │ └───────┼─────────────┼────────────────┼─────────────┼────────┘ │ └───────┌────────┘ │ │ │ │ ┌───────▼─────────────────────▼──────────────────────▼────────┐ │ Zookeper │ └─────────────────────────────────────────────────────────────┘
- Abaixo temos uma formação de um cluster com 4 brokers, para administrar estes, utiliza-se o Zookeeper.
-
-
Apache Avro:
-
Quando tratamos de eventos, podemos ter qualquer mensagem (string, xml, json). Todavia, em um contrato para as comunicações apiREST, utilizamos JSON.
- O Apache Avro, é basicamente um sistema de serialização de dados que trabalha com JSON;
- Fornece uma rica estrutura de dados;
- Oferece um formado de dado binário, compacto e rápido;
- É um container para gravar dados persistentes.
- Quem define o Avro é um JSON com propriedades (schema), por exemplo:
{ "namespace": "com.exemple.avro", "type": "record", "name": "User", "fields":[ {"name": "name", "type": "string"}, {"name": "favorite_number", "type": ["int", "null"]}, {"name": "favorite_color", "type": ["string", "null"]} ] }
{ "name": "Fulano Ciclano", "favorite_number": 7, "favorite_color": "purple" }
-
-
Schema Registry:
- É uma camada distribuída de armazenamento para Schemas Avro, o qual usa o Kafka como mecanismo de armazenamento;
- Responsável por gravar todos schemas avro e fazer a verificação de compatibilidade, antes de postar uma mensagem pelo produtor e antes de consumir pelo consumidor;
-
Serviços de Stream
- Microsoft Event Hub
- Amazon Kinesis
- Google Pub/Sub
- Kafka Local com Docker
-
Stack:
-
Back:
- Spring Boot: Para facilitar o processo de configuração e publicação de aplicações;
- Spring Web: Para expor end-points via rest;
- Spring Cloud Stream: Para nosso kafka;
- Spring Cloud Sleuth: Para gerar os logs da aplicação de forma organizada.
-
Front:
- html
- Bootstrap
-
Inicializaremos o nosso projeto através do spring initializr utilizando os parâmetros abaixo:
- Project: Gradle Project;
- Language: Java;
- Spring Boot: 2.4.5;
- Project Metadata:
- Group: br.com.ecommerce
- Artifact: checkout
- Name: checkout
- Description: Checkout API
- Package name: br.com.ecommerce.checkout
- Packaging: jar
- Java: 8
- Dependencies: Spring Web, Sleuth, Cloud Stream, Stream for Apache Kafka Streams, Spring Data JPA, PostgreSQL Driver, Lombok
O SonarCloud é uma plataforma em nuvem para exibir o processo de inspeção continua do código de sua aplicação. Para isso, o SonarCloud utiliza o SonarQube para realizar a “varredura” em seu código e analisar possíveis vulnerabilidade, erros e regras específicas da linguagem (Code Smells).
- Para inicializarmos a integração, acessaremos https://sonarcloud.io/projects
- Na parde superior direita, no símbolo [+], click no item
analyze new project
- Escolheremos a organização onde se encontra o repositório a ser analisado
(
Import another organization
, caso não haja nenhuma para selecionar direto); - Escolheremos a opção
Manually
- Para este projeto, em
What option best describer your build?
, selecionaremosGradle
- Agora basta seguir os passos que irão aparecer:
- IMPORTANTE: reiniciar a IDE após a configuração da variável de ambiente
SONAR_TOKEN
. - Para projetos com Java 8, será necessário alterar o Java para 11 ou superior (EXEMPLO abaixo):
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_281
set JAVA_HOME=C:\Users\Caiuzu\.jdks\adopt-openjdk-11.0.11
- IMPORTANTE: reiniciar a IDE após a configuração da variável de ambiente
-
Em build.gradle, adicionaremos as dependências abaixo:
ext { set('swaggerVersion', "2.9.2") } dependencies { // Swagger implementation "io.springfox:springfox-swagger2:${springCloudVersion}" implementation "io.springfox:springfox-swagger-ui:${springCloudVersion}" }
-
Iremos adicionar a anotação
@EnableSwagger2
em nosso main -
Em seguida, criaremos o diretório config, que será destinado a todas as configurações do nosso projeto. Dentro iremos criar a classe de configuração; SwaggerConfiguration
- Para funcionamento básico do swagger, devemos adicionar apenas as linhas abaixo. Para configurações adicionais, podemos utilizar os outros métodos contidos na classe (Autenticação, informações sobre o projeto, etc).
@EnableSwagger2 @Configuration public class SwaggerConfiguration { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("br.com.ecommerce.checkout")) .paths(PathSelectors.any()) .build(); } }
- Desta forma, já estamos prontos para o swagger através da URL: http://localhost:8080/swagger-ui.html
Basta criar um arquivo chamado Banner.txt, no diretório resources.
Spring Boot Actuator é um sub-projeto do Spring Boot Framework. Inclui vários recursos adicionais que nos ajudam a monitorar e gerir o aplicativo Spring Boot. Ele usa endpoints HTTP ou beans JMX para nos permitir interagir com ele. Expõe informações operacionais sobre o aplicativo em execução — integridade, métricas, informações, etc.
-
Adicionaremos as dependências para o actuator no arquivo build.gradle:
ext { set('springBootVersion', "2.4.5") } dependencies { // Spring Boot implementation "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" }
-
Em application.yml, iremos colocar as propriedades abaixo:
management: endpoint: health: enabled: true show-details: always
-
Desta forma, o actuator estará pronto, basta acessar: http://localhost:8080/actuator/
Esse Plugin tem a função de facilitar a conversão de um schema em uma classe java com apenas um comando. Por exemplo, digamos que nosso schema esteja definido (schema exemplo abaixo), na teoria teríamos que traduzir, manualmente cada definição de nosso arquivo para uma classe java, tornando o processo moroso.
{
"type": "record",
"name": "CheckoutCreateEvent",
"namespace": "br.com.ecommerce.checkout.event",
"fields": [
{
"name": "checkoutCode",
"type": ["string", "null"]
}
]
}
- Para configurar, primeiro, adicionaremos as dependências abaixo em nosso arquivo build.gradle:
plugins {
id 'com.commercehub.gradle.plugin.avro' version '0.99.99'
}
repositories {
mavenCentral()
maven {
url 'http://packages.confluent.io/maven/'
allowInsecureProtocol(true)
}
}
dependencies {
// Avro
implementation 'io.confluent:kafka-avro-serializer:5.5.0'
}
avro {
fieldVisibility = "PRIVATE"
}
generateAvroJava {
source 'src/main/resources/avro'
}
generateTestAvroJava {
source 'src/main/resources/avro'
}
-
Em seguida, criaremos uma pasta nomeada avro dentro de resources, na qual serão colocados nossos schemas e lidos para conversão.
-
Finalmente, para efetivamente gerar a classe, basta utilizar o comando no terminal:
gradlew generateAvroJava
Antes de seguir os passos abaixo, garanta que seu docker está instalada conforme explicado no inicio deste documento.
O docker, é basicamente um container. Ele usa os próprios recursos do kernel de nosso SO para "simular" uma nova máquina. Diferente de como faz uma VM (que gera um novo SO para realizar esta tarefa).
O docker-compose faz a orquestração desses containers. Assim, possibilitando uma infra local rápida e eficiente.
Iremos criar um diretório docker em nosso projeto e criaremos o arquivo de configuração docker-compose.yml.
Comandos mais utilizados (antes de utiliza-los, devemos estar no diretório, no terminal):
- iniciar:
docker-compose up --build -d
- listar containers:
docker ps
- derrubar os container e remover:
docker-compose down
- bonus - verificar se a porta está aberta:
telnet localhost {porta}
Primeiro, temos que identificar o que queremos conteinerizar. Para este projeto serão os seguinte itens: Banco de Dados(database-checkout e database-payment), Zookeerper, Kafka e Schema Registry;
Antes, precisamos entender cada linha de nosso docker-compose.yml
version: '3.7'
services:
database-checkout:
# image to fetch from docker hub
image: postgres:latest
# Environment variables for startup script
# container will use these variables
# to start the container with these define variables.
environment:
POSTGRES_DB: checkout
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
# Mapping of container port to host
ports:
- 5432:5432
version ‘3.7’
: Isso indica que estamos usando a versão 3.7 do Docker Compose, e o Docker fornecerá os recursos apropriados.
services
: Esta seção define todos os diferentes contêineres que criaremos. Em nosso projeto, temos cinco serviços (dois bancos, kafka, etc).
database-checkout
: Este é o nome do nosso serviço de banco de dados. O Docker Compose criará contêineres com o nome que fornecemos.
image
: Se não tivermos um Dockerfile e quisermos executar um serviço usando uma imagem pré-construída, especificamos o local da imagem usando a cláusula image. O Compose fará um fork de um contêiner dessa imagem.
ports
: Isso é usado para mapear as portas do contêiner para a máquina host.
environment
: A cláusula nos permite configurar uma variável de ambiente no contêiner. É o mesmo que o argumento -e no Docker ao executar um contêiner.
- Os parâmetros
POSTGRES_PASSWORD
,POSTGRES_USER
,POSTGRES_DB
, indica ao docker, para inicializar nosso banco de dados com o usuário de conexão pré-configurado.
Spring Data é um projeto SpringSource de alto nível cujo objetivo é unificar e facilitar o acesso a diferentes tipos de armazenamentos de persistência, tanto sistemas de banco de dados relacionais quanto armazenamentos de dados NoSQL
O Hibernate é um framework ORM, ou seja, a implementação física do que você usará para persistir, remover, atualizar ou buscar dados no SGBD. Por outro lado, o JPA (Java Persistence API) é uma camada que descreve uma interface comum para frameworks ORM.
Utilizando as configurações que definimos para nosso banco (em nosso caso, os passados em nosso docker-compose.yml, para database-checkout
),
iremos informar ao spring os dados para conexão da seguinte forma no arquivo application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/checkout # jdbc:driver://url_de_conexão:porta/banco_de_dados
username: admin # usuário do banco
password: admin # senha do banco
driver-class-name: org.postgresql.Driver # driver
hikari: #
connection-test-query: select 1 # consulta que será executada pouco antes de uma conexão do pool ser fornecida para validar se a conexão com o banco de dados está ativa
Iremos fazer configurações do hibernate jpa
jpa:
hibernate:
ddl-auto: create-drop #Cria e então destrói o schema no final da sessão.
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
show_sql: true
use_sql_comments: true
jdbc:
lob:
non_contextual_creation: true
Mais detalhes sobre os campos de configuração JPA/Hibernate:
jpa
:hibernate
:ddl-auto
: vai pegar a classe de entidade e irá gerar uma query de criação automaticamente de acordo com o parâmetro escolhido- none : Não gerar automaticamente. SEMPRE USAR ESTA OPÇÃO EM PROD e as outras apenas para teste/dev/local.
- validate: validar o schema, não faz mudanças no banco de dados.
- update: faz update o schema.
- create: cria o schema, destruindo dados anteriores.
- create-drop: Cria e então destrói o schema no final da sessão.
properties
:hibernate
:dialect
: especifica o dialeto que será usadoformat_sql
: formatação do sql ao exibir no console [ true | false ]show_sql
: exibir sql no console [ true | false ]use_sql_comments
: mostrar comentários no console [ true | false ]jdbc
:lob
: um lob é um objeto grande. As colunas Lob podem ser usadas para armazenar textos muito longos ou arquivos binários. Existem dois tipos de lobs: CLOB e BLOB. O primeiro é um lob de caracteres e pode ser usado para armazenar textos.non_contextual_creation
: criar lob no contexto [ true | false ]
Primeiramente precisamos montar nosso contrato de API. Quando fazemos APIs pensando primeiro no contrato (contract first), trazemos um nível de maturidade muito maior de entendimento da solução como um todo, antes mesmo de começar a programar. Além de iniciar o desenvolvimento com um artefato (contrato) que pode agilizar geração de código fonte, mocks, documentação etc.
Esse item é de grande importância, tanto para o back quanto para que o front possa trabalhar em paralelo, tendo como base fiel, os dados que serão expostos pela nossa API.
Tópicos a serem estudados: Testes de Contrato de API e Testes de Contrato de API com JOI
POST http://localhost:8085/v1/checkout/
Content-Type: application/json
{
"address": "string",
"cardCvv": "string",
"cardDate": "string",
"cardName": "string",
"cardNumber": "string",
"cep": "string",
"complement": "string",
"country": "string",
"email": "string",
"firstName": "string",
"lastName": "string",
"paymentMethod": "string",
"products": [
"string"
],
"saveAddress": true,
"saveInfo": true,
"state": "string"
}
ecommerce-checkout-api/
├── docker/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ ├── br.com.ecommerce.checkout/
│ │ │ │ ├── config/
│ │ │ │ ├── entity/
│ │ │ │ ├── listener/
│ │ │ │ ├── repository/
│ │ │ │ ├── resource.checkout/
│ │ │ │ ├── service/
│ │ │ │ ├── streaming/
│ │ │ │ └─ CheckoutApplication.java
│ │ │ ├── checkout.event/
│ │ │ └── payment.event/
│ │ └── recources/
│ │ ├── avro/
│ │ ├── application.yml
│ │ └── banner.txt
│ └── test/
├─ .gitignore
├─ build.gradle
├─ settings.gradle
└─ README.md
Para este projeto aplicaremos alguns conceitos de SOLID:
O S.O.L.I.D é um acrônimo que representa cinco princípios da programação orientada a objetos e design de códigos teorizados pelo nosso querido Uncle Bob (Robert C. Martin) por volta do ano 2000. O autor Michael Feathers foi responsável pela criação do acrônimo:
- S ingle Responsibility Principle (Princípio da Responsabilidade Única);
- O pen/Closed Principle (Princípio do Aberto/Fechado);
- L iskov Substitution Principle (Princípio da Substituição de Liskov);
- I nterface Segregation Principle (Princípio da Segregação de Interfaces);
- D ependency Inversion Principle (Princípio da Inversão de Dependências).
Mais sobre o assunto acessando: Princípios de SOLID.
Também utilizaremos lombok:
O Lombok é um Framework criado sob licença MIT, podendo ser usado livremente em qualquer projeto Java. Seu principal objetivo é diminuir a verbosidade das classes de mapeamento JPA, DTOs e Beans por exemplo.
-
CheckoutResource: Que será nossa controller, responsável pelo processamento das requisições e por gerar respostas;
- Iremos anotar nossa classe com
@Controller
para indicar ao spring que nossa classe é uma controller; @RequestMapping("/v1/checkout")
para mapear qual a path padrão para nosso resource;- Então criaremos nosso método
create ()
- Deverá ser anotado com
@PostMapping("/")
, para atender as requisições POST; - Receberá como parâmetro um objeto
CheckoutRequest
e responderá umResponseEntity<CheckoutResponse>
;@PostMapping("/") public ResponseEntity<CheckoutResponse> create(@RequestBody CheckoutRequest checkoutRequest) {}
- Deverá ser anotado com
- Iremos anotar nossa classe com
-
CheckoutRequest: Que será como um VO, usado basicamente para receber os dados serializados passados na request de forma que possa ser facilmente repassado a nossa Entidade.
- Iremos anotar nossa classe com
lombok.Data
para a criação automática de getters e setters; - Para gerar nossos contrutores em tempo de compilaçao, para TODOS os argumentos, anotaremos com
lombok.AllArgsConstructor
, e para caso não tenha nenhumlombok.NoArgsConstructor
; - Como é uma resquest, ela precisa ser serializada, então, implementaremos a interface
java.io.Serializable
; - Então, iremos declarar os dados que mapeamos no contrato, os quais receberemos em nossa em requisição;
- Iremos anotar nossa classe com
-
CheckoutRespose: Que será como um DTO, que representa nosso contrato de saída, usado basicamente para enviar os dado de retorno da nossa API para o cliente;
- Criaremos nossa entidade CheckoutEntity:
-
Ela representará os dados da tabela de nosso banco de dados de checkout;
-
Anotaremos com
@Entity
do pacotejavax.persistence.*
; -
E anotaremos cada representação de coluna com sua anotação representativa:
Breve entendimento sobre notações JPA: #TO-DO
@Id
:
@Column
:
@OneToOne
:
@OneToMany
: -
Será usada em nossa camada de repositório para ser acessada.
-
Anotaremos a classe com
lombok.Builder
,- Breve Explicação: Geralmente instanciamos classes da forma demonstrada abaixo, porém ao mudar parâmetros no construtor,
somos obrigados a alterar em todos que a utilizam:
final CheckoutEntity checkoutEntity = new CheckoutEntity();
- Para isso o lombok facilitará em nosso desenvolvimento, de forma que não precisaremos gerar um builder[*] na mão,
precisaremos apenas utilizar o
@Builder
em nossa Entidade, para que em nosso service possamos utilizar da seguinte forma:final CheckoutEntity checkoutEntity = CheckoutEntity.builder().code().build(); // A funcionalidade foi exatrída para o método getCheckoutEntity()
- Breve Explicação: Geralmente instanciamos classes da forma demonstrada abaixo, porém ao mudar parâmetros no construtor,
somos obrigados a alterar em todos que a utilizam:
-
[*] Um pouco sobre o Pattern Builder:
Pattern Builder, é um padrão de design projetado para fornecer uma solução flexível para vários problemas de criação de objetos na programação orientada a objetos. A intenção do padrão de projeto Builder é separar a construção de um objeto complexo de sua representação.
O padrão Builder, da forma como foi descrito no livro Design Patterns: Elements of Reusable Object-Oriented Software, contém os seguintes elementos:
- Permite variar a representação interna de um objeto;
- Encapsula o código entre construção e representação;
- Provê controle durante o processo de construção.
- Requer criar um concrete builder específico para cada instância diferente do produto.
-
Sendo assim, criaremos nosso repositório CheckoutRepository:
- Está classe fará acesso aos dados de nossa entidade;
- Deveremos anotar nossa classe com
org.springframework.stereotype.Repository
, para informar ao spring que nossa classe será um repositório - Herdaremos a classe
JpaRepositoroty<CheckoutEntty, Long>
, passando nossa entidade e a tipo do ID
-
Iremos criar nossa classe de serviços CheckoutService:
-
Deveremos anotar nossa classe com
org.springframework.stereotype.Service
, para que seja criado uma instância do nosso serviço; -
Iremos realizar a injeção de depedência de nosso
repositório
em nossoservice
, para que possamos utilizar os métodos JPA:- Comumente, para realizar a injeção de dependências é criado um construtor, como demonstrado abaixo:
private final CheckoutRepository checkoutRepository; public CheckoutService(final CheckoutRepository checkoutRepository) { this.checkoutRepository = checkoutRepository; }
- Porém, utilizando a anotação
lombok.RequiredArgsConstructor
, o construtor será criado em tempo de compilação para todos atributos que estejam comofinal
, fazendo com que seja somente necessário a linha abaixo:private final CheckoutRepository checkoutRepository;
AVISO! NUNCA UTILIZE
@AUTOWIRED
NO ATRIBUTO DA CLASSE PARA FAZER INJEÇÃO, É CRIME. SUJEITO A PAULADA!!!
- Porém, utilizando a anotação
- Sabendo disso, nosso Service também deverá ser injetado em nossa classe controller CheckoutResource)
- Comumente, para realizar a injeção de dependências é criado um construtor, como demonstrado abaixo:
-
Teremos em nosso service dois métodos.
- create: responsável por criar;
- Utilizaremos a api
Optional<>
, ela permite trabalhar com objetos nulos. - Para utilizarmos o
save()
passaremos a instância de nossa entidade após manipularmos utilizando as ferramentas proporcionadas pelo@Build
em nossorepository
final CheckoutEntity entity = checkoutRepository.save(checkoutEntity);
- Utilizaremos a api
- create: responsável por criar;
-
Com nosso sistema salvando os dados no banco, agora, precisaremos enviar um evento no kafka para dizer que o checkout foi criado:
-
Definiremos em nosso avro de CheckoutCreated.avsc os dados
checkoutCode
estatus
;-
Tudo que produzirmos e jogarmos nesse
tópico virtual
, para qualtópico real
ele será enviado?- Isso será definido em nossas configurações em application.yml
cloud: stream: kafka: binder: # Configurações para definir quem vai ser a ferramenta para messageria ou streaming (Kafka ou Rebbit) autoCreateTopics: true # No momento de subir a aplicação ele cria um tópico automático, semelhante ao ddl do JPA brokers: localhost:9092 # configura quem é o broker, poderia ter uma lista (porta default do kafka 9092) bindings: checkout-created-output: # Tópico Virtual destination: streaming.ecommerce.checkout.created # Tópico Real. Padrão de nomenclatura -> tipo_de_informação.nome_de_domino.entidade.ação_realizada contentType: application/*+avro # ContentType HTTP producer: use-native-encoding: true # usar o encoding nativo, o serializer e deserializer da confluent que definimos acima payment-paid-input: destination: streaming.ecommerce.payment.paid contentType: application/*+avro group: ${spring.application.name} consumer: use-native-decoding: true kafka: properties: schema: registry: url: http://localhost:8081 specific: avro: reader: true # define como true a propriedade de leitura producer: # Utilizaremos o Serializer e o Deserializer da confluent. Estes, já usam o schema registry, já possuí implementado a logica de pegar no schema registry e realizar validação do schema avro para manter a compatibilidade key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: io.confluent.kafka.serializers.KafkaAvroSerializer consumer: key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer
- Isso será definido em nossas configurações em application.yml
-
Explicação de organização de nomenclaturas para nomes de tópicos:
A medida que a empresa cresce, crescem também o numero de aplicações que estarão consumindo tópicos. Para isto, é importante ter uma organização dos tópicos bem definida
Configuraremos nosso padrão de nomenclatura em destination:
bindings: checkout-created-output: destination: streaming.ecommerce.checkout.created
streaming.ecommerce.checkout.created
-> tipo_de_informação.nome_de_domino.entidade.ação_realizada- tipo_de_informação: se é um
streaming
, ouetl
- nome_de_domino: qual domínio da aplicação
- entidade: a entidade que estamos publicando
- ação_realizada: create, update, etc
- tipo_de_informação: se é um
-
Anteriormente, executávamos ferramentas de linha de comando para criar tópicos no Kafka:
docker exec -t broker kafka-topics --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic streaming.ecommerce.checkout.created --if-not-exists
docker exec -t broker kafka-topics --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic streaming.ecommerce.payment.paid --if-not-exists
Mas, com a introdução do AdminClient no Kafka, agora podemos criar tópicos de maneira programática.
Precisamos adicionar o bean KafkaAdmin Spring, que adicionará tópicos automaticamente para todos os beans do tipo NewTopic:
@Configuration
public class KafkaTopicConfig {
@Value(value = "${kafka.bootstrapAddress}")
private String bootstrapAddress;
@Bean
public KafkaAdmin kafkaAdmin() {
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
return new KafkaAdmin(configs);
}
@Bean
public NewTopic topic1() {
return new NewTopic("checkout", 1, (short) 1);
}
}
-
Precisaremos criar uma classe de configuração de streaming StreamingConfig
- Anotaremos com:
@Configuration
do pacoteorg.springframework.context.annotation.Configuration
;- Utilizaremos o bean
@Value("${}")
, para resgatar os valores de nosso application.yml@Value("${spring.kafka.properties.schema.registry.url}") private String schemaRegistryUrl; @Value("${spring.cloud.stream.bindings.checkout-created-output.destination}") private String defaultTopic;
- Anotaremos com:
-
Para criar mensagens, primeiro precisamos configurar um ProducerFactory. Para isto, criaremos na classe de configuração StreamingConfig, nossos métodos. Isso definirá a estratégia para criar instâncias do Kafka Producer.
private ProducerFactory<String, CheckoutCreatedEvent> producerFactory(final KafkaProperties kafkaProperties) { Map<String, Object> configProps = kafkaProperties.buildProducerProperties(); configProps.put(KafkaAvroSerializerConfig.SCHEMA_REGISTRY_URL_CONFIG, schemaRegistryUrl); return new DefaultKafkaProducerFactory<>(configProps); }
-
Em seguida, precisamos de um KafkaTemplate, que envolve uma instância do Produtor e fornece métodos convenientes para enviar mensagens aos tópicos do Kafka.
@Bean public KafkaTemplate<String, CheckoutCreatedEvent> kafkaTemplate(final KafkaProperties kafkaProperties) { val kafkaTemplate = new KafkaTemplate<>(producerFactory(kafkaProperties)); kafkaTemplate.setDefaultTopic(defaultTopic); return kafkaTemplate; }
-
As instâncias do produtor são thread-safe. Portanto, o uso de uma única instância em todo o contexto do aplicativo proporcionará melhor desempenho. Consequentemente, as instâncias KakfaTemplate também são thread-safe e o uso de uma instância é recomendado.
-
Para publicar no kafka, podemos enviar mensagens usando a classe KafkaTemplate. Para isso, injetaremos a classe em nosso service CheckoutService e utilizaremos os métodos configurados em nosso KafkaTemplate
private final KafkaTemplate<String, CheckoutCreatedEvent> kafkaTemplate;
-
Chamaremos nossa instância de KafkaTemplate, e iremos enviar uma mensagem dentro do nosso método
send()
:
kafkaTemplate.send(MessageBuilder.withPayload(checkoutCreatedEvent).build());
-
Para consumir mensagens, precisamos configurar um
ConsumerFactory
e umKafkaListenerContainerFactory
. Uma vez que esses beans estão disponíveis na fábrica de bean Spring, os consumidores baseados em POJO podem ser configurados usando a anotação@KafkaListener
. -
A anotação
@EnableKafka
é necessária na classe de configuração (ou no contexto da aplicação CheckoutApplication) para permitir a detecção da anotação@KafkaListener
em beans gerenciados por spring: -
Em seguida, precisamos configurar o
ConsumerFactory
. Para isto, criaremos mais um método em nossa classe de configuração StreamingConfig. Isso definirá a estratégia para criar instâncias do Kafka Consumer.private ConsumerFactory<String, PaymentCreatedEvent> consumerFactory(final KafkaProperties kafkaProperties) { Map<String, Object> props = kafkaProperties.buildConsumerProperties(); KafkaAvroDeserializer kafkaAvroDeserializer = new KafkaAvroDeserializer(); kafkaAvroDeserializer.configure(props, false); return new DefaultKafkaConsumerFactory(props, new StringDeserializer(), kafkaAvroDeserializer); } @Bean public ConcurrentKafkaListenerContainerFactory<String, PaymentCreatedEvent> kafkaListenerContainerFactory(final KafkaProperties kafkaProperties) { ConcurrentKafkaListenerContainerFactory<String, PaymentCreatedEvent> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConcurrency(1); // definiremos a concorrência para 1 factory.setConsumerFactory(consumerFactory(kafkaProperties)); factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD); // definiremos nosso AcknowledgingMessage como RECORD - Confirme o deslocamento após cada registro ser processado pelo ouvinte. return factory; }
Mais sobre ContainerProperties.AckMode
-
Em nosso projeto Checkout, consumiremos o retorno de Payment. Logo, criaremos um listener para utilizar as configurações acima:
- Vamos criar a classe PaymentPaidListener e utilizaremos o Bean
@KafkaListener(topics = "${}", groupId = "${}")
para escutar o tópico com o retorno de Payment:@KafkaListener(topics = "${spring.cloud.stream.bindings.payment-paid-input.destination}", groupId = "${spring.cloud.stream.bindings.payment-paid-input.group}")
- Em seguida iremos utilizar nossa classe PaymentCreatedEvent, gerada por nosso avro, para resgatar nosso checkoutCode e atualizar no banco de dados.
public void handler(PaymentCreatedEvent paymentCreatedEvent) { checkoutService.updateStatus(paymentCreatedEvent.getCheckoutCode().toString(), Status.APPROVED); }
- Vamos criar a classe PaymentPaidListener e utilizaremos o Bean
-
Subjects: localhost:8081/subjects
[ "streaming.ecommerce.checkout.created-value" ]
-
Subjects: localhost:8081/subjects/streaming.ecommerce.checkout.created-value/versions/latest
[ "streaming.ecommerce.checkout.created-value" ]
Dessa forma, finalizamos nossa API de Checkout e podemos dar início a nossa Api de Payment, que atuara como nosso consumer.
- Utilizar
javax.validation.constraints
(@NotEmpty
, etc), para validar os campo em CheckoutRequest - TDD
- BDD (Cucumber)
- Ajustar bibliotecas depreciadas
- Jenkins
- Hystrix