Pular para o conteúdo principal

Construindo uma Aplicação Completa com Kotlin, Spring Boot, Docker e Azure

 Hoje iremos construir uma aplicação completa utilizando Kotlin, Spring, Docker e com deploy no Azure, esta aplicação será responsável por realizar o cálculo do IMC.

Os Requisitos

A aplicação deve realizar o cálculo do IMC (Índice de Massa Corporal), portanto teremos uma Api que receberá os seguintes parâmetros:
  • Altura (Double);
  • Peso (Double).
O retorno será um JSON contendo uma mensagem sobre em qual faixa a pessoa se encontra.

Criação da Aplicação

A aplicação será construída utilizando as seguintes tecnologias:

Estrutura

No site do Spring Initializr vamos utilizar o Generate a Project, que é um wizard que já cria o projeto e coloca todas as dependências necessárias.

Criando a Aplicação no Spring Initializr.


Após o download do projeto, descompacte e o importe na IDE, o resultado será como o abaixo:

Projeto dentro do IntelliJ IDEA.

Arquitetura

Como se trata de uma aplicação pequena não precisamos de uma arquitetura muito elaborada, e sim uma que atenda a alguns requisitos básicos:
  • Não tenha estado entre as chamadas;
  • Possa ser utilizada por qualquer cliente, ou seja, qualquer aplicação independente da tecnologia pode se comunicar com a API;
  • As regras de cálculo deve ser simples e com possibilidade de extensão.
Com base nos requisitos, iremos seguir o modelo de Clean Architecture  proposta por Uncle Bob, assim temos cada parte da aplicação isolada, facilitando uma evolução ou alteração futura.


Arquitetura da Aplicação.

Acima definimos a arquitetura, onde teremos os seguintes packages:
  • application
    • Responsável por armazenar conteúdo de borda da aplicação, no nosso caso, somente teremos o entrypoints, onde teremos a API e classe de Request;
  • core
    • Responsável por armazenar o conteúdo de domain e os usecases da aplicação;
  • config
    • Contém todos os recursos de configuração da aplicação, tais como: Classes de tratamento de erros e Bundles de mensagens;


Desenvolvimento da Aplicação

Agora que já temos a base desenvolvida, vamos passar pelos principais pontos do código, analisando como o fluxo da tarefa acontece.
Aplicação Desenvolvida.

Resources

O arquivo de configuração de aplicações Spring Boot ficam armazenados em src/main/resources, eles podem ser Properties ou Yaml, dentro desse arquivo adicionamos diversas configurações, tais como, portas, profiles, databases, etc.

Em nossa aplicação teremos 2 ambientes, sendo o primeiro de desenvolvimento local (dev), onde o serviço irá utilizar a porta 8080, e em produção(prod) usando a porta 80, para termos a distinção entre os ambientes usaremos os profiles do Spring Boot, onde podemos ter configuração especificas para cada ambiente de execução.

Arquivos para os profiles da aplicação.

Perceba que temos arquivo properties delimitados por nome, dessa forma conseguimos estabelecer configurações diferentes para cada ambiente.


Entrypoints

O porta de entrada da aplicação onde temos a API e a classe de Request, aqui iremos receber os seguintes dados do usuário:
  • height: Altura;
  • weight: Peso.
Como sempre, temos que pensar na validação dos dados de entrada, por isso usaremos o BeanValidation para conseguir validar e devolver uma mensagem amigável ao usuário.

@RestController
class ImcApi(val imc:ImcUsecase) {

    @PostMapping("/imc")
    fun imc(@Valid @RequestBody imcRequest: ImcRequest): ResponseEntity<ImcResponse> {

        return ResponseEntity.ok(imc.calcule(imcRequest.height!!, imcRequest.weight!!))
    }
}
API

class ImcRequest {

    @NotNull(message="{message.height.notnull}")
    var height: Double? = null

    @NotNull(message="{message.weight.notnull}")
    var weight:Double? = null
}
Classe de Request

Esta camada é bem simples, e com Kotlin poderíamos deixar-lá ainda menor, omitindo valores de retorno e colocando os recursos no mesmo arquivo.

Config

A camada de config é onde colocamos as classes de configuração da aplicação, sendo elas detalhes de framework, tratamento de erros, entre outras configurações.

O tratamento de erros é um exemplo de configuração customizada, por padrão, o Spring nos fornece um JSON com informações referente a erros de validação que aconteceram em uma chamada, mas este não tão amigável para quem vai consumir, portanto criamos um @RestControllerAdvice para fornecer uma resposta mais simples.

open class ErrorMessage(open val message:String)

data class FieldErrorMessage(override val message:String,
                                      val field:String) : ErrorMessage(message)

@RestControllerAdvice
class ErrorController(val messageSource: MessageSource) {

    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = [MethodArgumentNotValidException::class])
    fun methodArgumentNotValidException(ex: MethodArgumentNotValidException): List<FieldErrorMessage>? {

        return ex.bindingResult.fieldErrors.map {
            FieldErrorMessage(messageSource.getMessage(it, LocaleContextHolder.getLocale()), it.field)
        }
    }

    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = [HttpMessageNotReadableException::class])
    fun methodArgumentNotValidException(ex: HttpMessageNotReadableException): ErrorMessage {

        return ErrorMessage(ex.localizedMessage)
    }
}
Classe customizada para tratamento de erros.

Core

A camada core é onde temos as principais lógicas da aplicação, sendo elas domain (Camada que envolve todas as regras de negocio) e os usecases (Regras de aplicação, também conhecida como Application Services).

Em nosso domain, temos um arquivo chamado Classify, nele temos um enum que armazena as regras sobre as faixas do IMC, e uma função que determina a classificação com base no argumento result, portanto, em caso de alguma mudança de regra ou uma possível nova faixa, apenas este elemento sofreria uma modificação:

fun defineClassification(result:Double):Classification{

    var classify:Classification? = null

    for(currentClassify in Classification.values()){

        if(currentClassify.predicate(result)){
            classify = currentClassify
            break
        }
    }

    return classify!!
}

enum class Classification(val category: String,
                          val table: String,
                          val predicate:(Double) -> Boolean){

    THINNESS("MAGREZA", "MENOR QUE 18,5", {result -> result <= 18.4}),
    NORMAL("NORMAL", "ENTRE 18,5 E 24,9", {result -> result in 18.5..24.9 }),
    OVERWEIGHT("SOBREPESO", "ENTRE 25,0 E 29,9", {result -> result in 25.0..29.9 }),
    OBESITY("OBESIDADE", "ENTRE 30,0 E 39,9", {result -> result in 30.0..39.9 }),
    SERIOUS_OBESITY("OBESIDADE GRAVE", "MAIOR QUE 40,0", {result -> result >= 40.0})
}
Regras de Classificação.

No usecase temos os elementos que vão orquestrar o processo de cálculo do IMC, aqui recebemos os dados de entrada, processamos, e os encaminhamos ao domain para determinar qual será a classificação de usuário:

@Component
class ImcUsecaseLogic : ImcUsecase {

    override fun calcule(height: Double, weight: Double): ImcResponse {

        val resultValue = weight / (height * height)

        val classifyResult = defineClassification(resultValue)

        return with(classifyResult){

            val resultBigDecimal = BigDecimal.valueOf(resultValue).setScale(2, RoundingMode.HALF_EVEN)

            ImcResponse(resultBigDecimal, category, table)
        }
    }
}
Regras de Aplicação do Cálculo.

Um outro ponto importante são os testes unitários, eles garantem que a lógicas de negocio da aplicação estão de acordo com os requisitos, aqui criamos alguns casos de testes para a classe ImcUsecaseLogic.

    @Test
    fun `Calcula o IMC com resultado = THINNESS`(){

        val height = 1.90
        val weight = 60.0

        imcUsecase.calcule(height, weight).run {

            assertEquals(BigDecimal.valueOf(16.62), result)
            assertEquals(category, THINNESS.category)
            assertEquals(message, THINNESS.table)
        }
    }
Caso de teste avaliando o resultado THINNESS.

Após a aplicação estar concluída, vamos testar usando a interface do Swagger, para isso vamos adicionar a seguinte dependência no pom.xml:

<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>3.0.0</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-boot-starter</artifactId>
	<version>3.0.0</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>3.0.0</version>
</dependency>

Com tudo pronto podemos rodar aplicação através da IDE ou utilizando Maven como abaixo:

mvn spring-boot:run

ou

./mvnw spring-boot:run

Após basta acessar a url http://localhost:8080/swagger-ui/index.html, e teremos o seguinte resultado:

Tela do Swagger.

Adicionando Docker ao Projeto

Atualmente o uso de containers vem ganhando popularidade, isso se deve principalmente a facilidade de implantação, agrupamento de dependências, entre outros requisitos, pois tudo isso esta encapsulado dentro da imagem.

Criando a imagem

Existem várias maneiras de se criar uma imagem para uma aplicação JVM, principalmente usando Spring Boot, vamos listar algumas delas:
  • Dockerfile com Multi-Stage Builds
    • O build da aplicação acontece em uma imagem própria para build, onde temos o compilador e o gerenciador de dependência (Maven ou Gradle), após utilizamos uma imagem apenas para a execução, dessa forma temos uma imagem de Build e outra de Execução;
  • Framework
    • Buildpack: No caso do Spring Boot, a partir da versão 2.3.0, temos a feature chamada Buildpack que permite a criação de imagens sem a necessidade do uso do Dockerfile, isso através do comando spring-boot:build-image;
    • Layered jar:  Por padrão o Spring Boot cria um fat-jar contendo todas as dependências da aplicação, isso não é muito bom para cenários com containers, pois tudo ficaria em uma unica layer, mas agora podemos quebrar o artefato em diferentes layers, e usando isso dentro da imagem docker.
Em nosso projeto vamos utilizar a primeira alternativa utilizando Dockerfile, dessa forma podemos analisar a estrutura de um arquivo Dockerfile.

O primeiro passo é estar dentro do diretório raiz da aplicação (onde estão o pom.xml e o Dockerfile) e criar o build da aplicação através do Maven, usando o comando:

mvn clean package

ou

./mvnw clean package

Após o build, vamos construir a imagem docker com o comando:

docker build -f app-imc:1.0.0 .

Agora vamos listar as imagens:

docker images

O Resultado será: 
Imagem criada com base no nome e versão presentes no pom.xml.

Subindo para o Docker Hub

A próxima etapa é subir a imagem criada para um registry, este é um repositório utilizado para armazenar imagens docker, aqui iremos utilizar o Docker Hub.

Conta Docker Hub

Para utilizar o Docker Hub precisamos criar uma conta, esta permite que você possa armazenar imagens públicas e também privados (sendo um grátis e os demais pagos).

Criando a Tag

O próximo passo é criar uma tag da imagem e enviá-la ao registry, aqui devemos utilizar a seguinte sintaxe: <login-registry>/<image-name>:<version> 

docker tag app-imc:1.0.0 aqui-o-seu-login/app-imc:1.0.0

Faça novamente um docker images e veremos a tag pronta para ser utilizada.

Login Docker Hub

Para realizar push para o Docker Hub é necessário efetuar o processo de autenticação, para isso devemos executar o seguinte comando:

docker login

O console irá pedir o Login e Senha, após o processo você deverá ver uma mensagem de Login Succeeded.

Realizando o Push

A ultima etapa é realizar o push para o Docker Hub, esta operação é a responsável por encaminhar a tag gerada para o servidor remoto.

docker push aqui-o-seu-login/app-imc:1.0.0

Após o push efetue o login na plataforma do Docker Hub, e vá em repositórios, o resultado será o seguinte:

Imagem armazenada no Docker Hub.

Após todos os passos temos a aplicação pronta para rodar em qualquer ambiente que tenha um container runtime.

Subindo a Aplicação no Azure

Agora que temos a aplicação pronta e já hospedada no Docker Hub, vamos subir no Azure para realmente termos um ciclo de desenvolvimento de ponta a ponta.

O Serviço que vamos utilizar no Azure é o Azure App Services, ele permite subir aplicações em uma infraestrutura gerenciada, ou seja, não temos que nos preocupar com detalhes de hardware ou coisas do tipo.

Abrindo a Conta no Azure

Antes de tudo precisamos ter uma conta no Azure, similar a outros cloud providers, o Azure nos fornece uma conta com vários serviços gratuitos por um período de 12 meses, inclusive existem alguns serviços que são sempre gratuitos.

Um outro fator interessante é que além dos serviços gratuitos, também teremos um crédito em $$$ para usar os serviços que não estão na tabela de serviços gratuitos.

A pagina inicial do Azure é como a abaixo:

Tela inicial do Portal Azure.

Realizando a implantação no Azure App Services

A implantação da aplicação vai acontecer utilizando o Azure App Services portanto, na tela inicial vá na opção App Services e teremos a tela abaixo:

Tela principal do Azure App Services.

Agora para criar uma nova aplicação, clique na opção New e teremos a seguinte tela:

Criação de um App Services.

Aqui iremos preencher os dados referentes a aplicação, que são os seguintes:
  • Subscription: Avaliação Gratuita
  • Resource Group: Vamos criar um novo chamado (app-imc-resource);
  • Name: Aqui o nome deverá ser único, ele será utilizado para o acesso a aplicação, por exemplo: <name>.azurewebsites.net
  • Publish: Aqui é onde especificamos a tecnologia, no nosso caso, selecionaremos Docker Container;
  • Region: Região onde a aplicação estará alocada, podemos manter East US;
  • App Service Plan: Como nossa aplicação é de testes, aqui manteremos as configuração do plano free (Free F1);
A próxima etapa de configuração é referente ao Docker, aqui teremos que adicionar as seguintes informações:
  • Options: Selecione Single Container;
  • Image Source: Aqui iremos selecionar o Docker Hub, que foi onde enviamos a nossa imagem;
  • Access Type: Selecione Public, por ser um repositório público;
  • Image and Tag: Aqui devemos adicionar o nome e tag da imagem no formato repositorio/nome_imagem:tag, exemplo: repositorio/app-imc:1.0.0
Na etapa de Monitoring, não iremos utilizar nenhum recurso, portanto, marque a opção Enable Application Insights como No.

Na etapa de Tags também não são necessários preenchimentos, portanto basta seguir para a ultima etapa de Review.

Review

Nesta etapa é onde visualizamos um resumo da configuração, é importante sempre revisar as informações principalmente as relacionadas ao custo, para aplicações de testes o item SKU deve ser Free:
Tela de Revisão.

Após a revisão devemos ir na opção Create, dessa forma o ambiente será criado conforme todas as configurações realizadas nas etapas anteriores.

Deployment em progresso.

Deployment

Após o processo de criação temos a tela de Deployment, aqui são exibidas informações que podem ser realizadas na aplicação,  além de fornecer gráficos contendo métricas de uso e consumo.

Tela de Deployment

Para acessar a aplicação basta acessar a seguinte url: 
<app-name>.azurewebsites.net/swagger-ui.index.html

Tela final do serviço executando no Azure App Services.

Código Fonte

Referências

Comentários

Postagens mais visitadas do Blog