Spring Boot Tutorial, Parte 2 - Projeto Base e 12-Factor App
No primeiro post da série, montamos nossa bancada de trabalho com Debian Trixie, SDKMAN travando o JDK 26 e o VS Codium livre de telemetrias. Com as ferramentas no lugar, o impulso natural da maioria dos tutoriais é abrir o editor e sair cuspindo código de negócio desordenado. Mas se queremos construir uma aplicação que sobreviva ao mundo real, precisamos falar antes sobre como uma aplicação nasce e como ela lida com seus ambientes.
🔔 Nota da Série: Este post faz parte da série “Spring Boot Tutorial”, onde construímos do zero uma API backend de produção com Spring Boot, explorando boas práticas de arquitetura, contratos, persistência, resiliência, observabilidade e nuvem. O código-fonte de apoio e os projetos de cada capítulo estão organizados no repositório parceiro vndmtrx/estudos_springboot.
Quem trabalha com infraestrutura e operações aprende rápido uma verdade incômoda: a esmagadora maioria dos incidentes de deploy não acontece por falha de sintaxe, mas por confusão de configuração. É o desenvolvedor que chumba a URL do banco local no meio do código, comita credenciais sensíveis no repositório ou assume que a aplicação vai rodar para sempre no mesmo caminho de diretório.
Para evitar esse tipo de armadilha desde o primeiro commit, o ecossistema de nuvem e microsserviços se apoia na metodologia dos 12-Factor App 1. Criada pelos desenvolvedores da plataforma Heroku, ela define doze princípios para criar aplicações portáveis, resilientes e escaláveis.
Neste momento da série, dois desses fatores nos interessam diretamente:
- Fator III (Configurações): Armazene as configurações estritamente no ambiente. A aplicação deve ser agnóstica a onde está rodando; o mesmo pacote binário compilado precisa servir tanto para o seu notebook quanto para o cluster de produção, mudando apenas as variáveis externas fornecidas a ele.
- Fator X (Paridade Dev/Prod): Mantenha o ambiente de desenvolvimento, homologação e produção o mais similares possível.
Com esses princípios em mente, vamos inicializar nosso projeto e estruturar sua gestão de perfis e configurações.
A inicialização do projeto com Maven e Spring Initializr
A forma mais limpa e padronizada de iniciar um projeto Spring Boot é através do Spring Initializr 2.
Se você abrir a página start.spring.io no navegador, encontrará uma interface visual completa para montar seu projeto do jeito que preferir:
- Project: Maven
- Language: Java
- Spring Boot: 4.1.x (ou a versão estável mais recente)
- Project Metadata: Group (
io.github.vndmtrx), Artifact (tarefas-api), Name (tarefas-api), Package name (io.github.vndmtrx.tarefas-api) - Configuration: YAML (
configurationFileFormat=yaml) - Packaging: Jar
- Java: 26
- Dependencies: Clicando no botão Add Dependencies (ou usando o atalho
Ctrl + B), basta buscar e adicionarSpring WebeSpring Boot DevTools.
Pela página, você pode clicar no botão GENERATE (ou teclar Ctrl + Enter), baixar o arquivo .zip gerado e descompactá-lo na pasta de trabalho.
A interface web é excelente para explorar dependências e visualizar opções. No entanto, para o nosso laboratório, podemos baixar o arquivo compactado da estrutura inicial diretamente via curl:
$ curl https://start.spring.io/starter.tgz \
-d type=maven-project \
-d language=java \
-d bootVersion=4.1.0 \
-d baseDir=tarefas-api \
-d configurationFileFormat=yaml \
-d groupId=io.github.vndmtrx \
-d artifactId=tarefas-api \
-d name=tarefas-api \
-d packageName=io.github.vndmtrx.tarefas-api \
-d javaVersion=26 \
-d dependencies=web,devtools \
-o tarefas-api.tgz
# Baixa o arquivo compactado com a estrutura base do projeto
Basta descompactar o arquivo .tgz no seu diretório de trabalho ou, se preferir, clonar e abrir diretamente o código deste capítulo no repositório parceiro.
Repare que na raiz do projeto temos o arquivo mvnw (o Maven Wrapper) 3. O Maven Wrapper é fundamental para a reprodutibilidade: ele garante que qualquer pessoa (ou servidor de CI) que clone o repositório execute exatamente a mesma versão do Maven sem precisar ter o binário instalado globalmente no sistema operacional.
Podemos compilar e rodar a aplicação imediatamente:
$ ./mvnw spring-boot:run
# Inicia a aplicacao usando o wrapper do Maven
A aplicação subirá em poucos segundos, escutando por padrão na porta 8080.
Se você abrir http://localhost:8080 no navegador (ou rodar curl -i http://localhost:8080 no terminal), se deparará com a famosa Whitelabel Error Page:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Aug 19 15:01:52 GMT-03:00 2026
There was an unexpected error (type=Not Found, status=404).
No static resource .
Não se assuste: esse comportamento é totalmente esperado. Ele confirma que o servidor embutido (Apache Tomcat) subiu com sucesso e que o despachante de requisições do Spring (DispatcherServlet) está ativo. Como ainda não criamos nenhum controller ou recurso estático para responder na rota raiz (/), o Spring retorna o status HTTP 404 Not Found.
Podemos encerrar a aplicação com Ctrl + C no terminal.
Configurações no padrão 12-Factor: application.yaml e variáveis de ambiente
Ao selecionarmos o formato YAML diretamente no Spring Initializr (ou através do parâmetro configurationFileFormat=yaml no nosso comando), o projeto já nasce com o arquivo src/main/resources/application.yaml nativo, dispensando o antigo formato .properties e entregando uma estrutura hierárquica e muito mais legível.
Agora vem o ponto crucial: como o Spring Boot consome configurações?
O Spring possui uma hierarquia sofisticada de resolução de propriedades chamada Environment Abstraction 4. Quando a aplicação precisa de um valor, ela busca em diversas fontes na seguinte ordem de prioridade (da mais alta para a mais baixa):
- Argumentos de linha de comando (
--app.mensagem="Texto") - Variáveis de ambiente do sistema operacional (
APP_MENSAGEM="Texto") - Arquivos de propriedades específicos de perfil (
application-dev.yaml) - Arquivo de propriedades padrão da aplicação (
application.yaml)
Isso significa que podemos definir valores padrão seguros para desenvolvimento local no application.yaml, enquanto permitimos que variáveis de ambiente sobrescrevam esses valores em outros ambientes sem tocar no código.
A sintaxe de interpolação com valor de reserva (fallback) do Spring Boot segue a estrutura ${NOME_VARIAVEL:valor_padrao}. Vamos editar o src/main/resources/application.yaml:
spring:
application:
name: tarefas-api
app:
mensagem: ${APP_MENSAGEM:Ola, bem-vindo a API de Tarefas!}
versao: "1.0.0"
Se executarmos a aplicação normalmente, ela usará a frase padrão "Ola, bem-vindo a API de Tarefas!". Mas se exportarmos uma variável no terminal antes de iniciar o processo:
$ APP_MENSAGEM="Servidor rodando em modo customizado" ./mvnw spring-boot:run
# Inicia o Spring Boot injetando a variavel de ambiente diretamente pelo shell
O Spring Boot detectará automaticamente a variável APP_MENSAGEM e substituirá o valor padrão. Nenhuma recompilação, nenhuma edição de arquivo. Isso é o 12-Factor App em ação.
Isolando ambientes com Spring Profiles e Maven
Em projetos reais, nem toda configuração se resume a uma única variável solta. Frequentemente precisamos alternar blocos inteiros de comportamento entre o ambiente de desenvolvimento local (dev) e o ambiente de produção (prod).
O Spring Boot resolve isso com o mecanismo de Profiles 5. A convenção é criar arquivos complementares seguindo o padrão de nomenclatura application-{profile}.yaml.
Vamos criar dois arquivos de perfil em src/main/resources/:
O arquivo de desenvolvimento local src/main/resources/application-dev.yaml:
app:
ambiente: "Desenvolvimento Local"
logging:
level:
root: INFO
"[org.springframework.web]": DEBUG
"[io.github.vndmtrx.tarefas_api]": DEBUG
E o arquivo de produção src/main/resources/application-prod.yaml:
app:
ambiente: "Producao"
logging:
level:
root: ERROR
"[io.github.vndmtrx.tarefas_api]": INFO
⚠️ Aviso: Repare no uso de colchetes e aspas duplas
"[nome.do.pacote]"soblogging.level: em arquivos YAML, como os pacotes Java contêm pontos (.), essa sintaxe de escape é a convenção oficial recomendada pelo Spring Boot para evitar que o interpretador YAML confunda o nome do pacote com nós aninhados 6.
- No perfil
dev, ativamos o nívelDEBUGpara a camada web e para o nosso pacoteio.github.vndmtrx.tarefas_api, permitindo inspecionar o roteamento e detalhes internos durante os testes. - No perfil
prod, elevamos o nível geral (root) paraERRORpara silenciar ruídos de bibliotecas de terceiros, liberando apenas mensagensINFOrelevantes do nosso próprio sistema.
Para ativar um perfil durante a execução, podemos usar três estratégias diferentes:
- Via argumento no terminal:
$ ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev # Ativa explicitamente o perfil dev - Via variável de ambiente (padrão em contêineres e Kubernetes):
$ SPRING_PROFILES_ACTIVE=prod ./mvnw spring-boot:run # Define o perfil ativo atraves de variavel de ambiente do sistema operacional - Via perfil ativo no arquivo base
application.yaml:spring: profiles: active: dev
Essa separação garante clareza absoluta: o arquivo application.yaml centraliza a estrutura geral, enquanto os arquivos de profile refinam apenas os valores específicos de cada contexto.
Inspecionando o perfil ativo nos logs de inicialização
O Spring Boot sempre informa com clareza nos logs de inicialização qual perfil está em execução. Repare na linha emitida logo abaixo do banner ASCII em cada cenário:
- Sem perfil informado (comportamento padrão):
INFO ... TarefasApiApplication : No active profile set, falling back to 1 default profile: "default" - Com o perfil
devativado (-Dspring-boot.run.profiles=dev):INFO ... TarefasApiApplication : The following 1 profile is active: "dev" - Com o perfil
prodativado (SPRING_PROFILES_ACTIVE=prod):INFO ... TarefasApiApplication : The following 1 profile is active: "prod"
Essa linha de log é o seu feedback visual imediato de que a sobreposição correta (application-dev.yaml ou application-prod.yaml) foi carregada com sucesso sobre o arquivo base.
Construindo o primeiro endpoint REST
Com o projeto inicializado e as configurações parametrizadas, vamos construir nosso primeiro endpoint REST para validar o fluxo de ponta a ponta.
No pacote io.github.vndmtrx.tarefas_api, criaremos um Java Record para representar a resposta estruturada de status da nossa API:
Arquivo src/main/java/io/github/vndmtrx/tarefas_api/StatusResposta.java:
package io.github.vndmtrx.tarefas_api;
public record StatusResposta(String status, String mensagem, String versao) {}
Em seguida, criamos o controller que responderá na rota /api/status:
Arquivo src/main/java/io/github/vndmtrx/tarefas_api/StatusController.java:
package io.github.vndmtrx.tarefas_api;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/status")
public class StatusController {
private final String mensagem;
private final String versao;
public StatusController(
@Value("${app.mensagem}") String mensagem,
@Value("${app.versao:1.0.0}") String versao) {
this.mensagem = mensagem;
this.versao = versao;
}
@GetMapping
public ResponseEntity<StatusResposta> verificarStatus() {
var resposta = new StatusResposta("UP", mensagem, versao);
return ResponseEntity.ok(resposta);
}
}
Preste atenção em um detalhe arquitetural fundamental: colocamos a anotação @Value nos parâmetros do construtor em vez de anotá-la diretamente nas variáveis de classe.
Há duas razões cruciais para essa escolha:
- Imutabilidade e rigor do compilador: Em Java, atributos declarados como
finalprecisam obrigatoriamente ser inicializados no momento da declaração ou no corpo do construtor. Se tentássemos fazer@Value(...) private final String mensagem;sem construtor, o compilador do Java geraria um erro imediato de compilação (“variable might not have been initialized”). Remover o modificadorfinalapenas para forçar o Spring a injetar via reflexão oculta quebra a imutabilidade e abre margem para mutações acidentais de estado. - Testabilidade e desacoplamento: Com injeção por construtor, o controller é uma classe Java pura (POJO). Não dependemos do container do Spring ou de reflexão para criar instâncias: podemos chamar
new StatusController("Texto", "1.0.0")em qualquer lugar, seja em testes de unidade puros ou no próprio terminal.
Para propriedades simples e pontuais como a nossa mensagem e versão, o @Value no construtor é a solução mais enxuta. Conforme a aplicação evoluir e acumular conjuntos complexos de propriedades (como regras de segurança, parâmetros de mensageria e tuning de conexões), veremos mais adiante como agrupar essas configurações em Records tipados usando @ConfigurationProperties.
Experimentação rápida: JShell no classpath do projeto
Antes de subirmos o servidor HTTP ou escrevermos arquivos de teste formais, podemos validar nosso record e nossos controllers diretamente no JShell com todo o ambiente do projeto carregado.
Para que o JShell enxergue tanto as nossas classes compiladas quanto as bibliotecas do Spring (como o ResponseEntity), precisamos de duas coisas simples:
- Compilar o projeto com o Maven para gerar o bytecode em
target/classes. - Gerar o caminho completo das dependências do Spring gerenciadas pelo Maven.
Fazemos isso em poucos comandos no terminal:
$ ./mvnw compile
# Compila os fontes Java e gera o bytecode em target/classes
$ ./mvnw dependency:build-classpath -Dmdep.outputFile=.classpath.txt
# Exporta a lista de JARs de dependencias do Spring para o arquivo .classpath.txt
$ jshell --class-path target/classes:$(cat .classpath.txt)
# Abre o REPL com acesso as nossas classes e a todo o ecossistema Spring
Dentro da sessão interativa, veja como podemos importar nossas classes, instanciar o record e chamar o método do controller diretamente:
jshell> import io.github.vndmtrx.tarefas_api.*;
jshell> var status = new StatusResposta("UP", "Testando interativamente", "1.0.0");
status ==> StatusResposta[status=UP, mensagem=Testando interativamente, versao=1.0.0]
jshell> var controller = new StatusController("Ola do JShell!", "1.0.0");
controller ==> io.github.vndmtrx.tarefas_api.StatusController@4de5031f
jshell> var resposta = controller.verificarStatus();
resposta ==> <200 OK OK,StatusResposta[status=UP, mensagem=Ola do JShell!, versao=1.0.0],[]>
jshell> resposta.getBody();
$4 ==> StatusResposta[status=UP, mensagem=Ola do JShell!, versao=1.0.0]
jshell> resposta.getStatusCode();
$5 ==> 200 OK
jshell> /exit
| Goodbye
Repare no grande ganho arquitetural: ao adotarmos injeção por construtor em vez de campos privados anotados, nosso controller se comporta como uma classe Java pura, 100% instanciável e testável de forma isolada sem depender de mágica ou reflexão oculta do framework.
No entanto, para validar o comportamento integrado em runtime (como o mapeamento de rotas HTTP no Tomcat e a serialização JSON), precisamos formalizar a garantia de qualidade com testes automatizados.
O contrato de testes unitários com MockMvc
Aqui entra a regra de ouro que adotaremos em toda a série: nenhum código avança sem testes automatizados.
A boa notícia é que não precisamos configurar bibliotecas de teste externas do zero: o Spring Initializr já inclui por padrão no nosso pom.xml o artefato spring-boot-starter-webmvc-test. Esse pacote empacota o JUnit 5, o Mockito e o utilitário MockMvc prontos para uso.
Vamos escrever um teste unitário para o StatusController utilizando o MockMvc. Com a anotação @WebMvcTest, o Spring inicializa exclusivamente a camada web (controllers e serialização JSON), permitindo simular requisições HTTP e validar o payload de resposta diretamente na memória, sem abrir portas de rede ou subir um servidor web completo:
Arquivo src/test/java/io/github/vndmtrx/tarefas_api/StatusControllerTest.java:
package io.github.vndmtrx.tarefas_api;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(StatusController.class)
class StatusControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("Deve retornar status 200 e payload de saude com sucesso")
void deveRetornarStatusComSucesso() throws Exception {
mockMvc.perform(get("/api/status")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"))
.andExpect(jsonPath("$.versao").value("1.0.0"));
}
}
Executamos a suite de testes no terminal:
$ ./mvnw test
# Executa todos os testes automatizados da aplicacao
O resultado deve confirmar 100% de sucesso para a suíte (incluindo o teste base TarefasApiApplicationTests gerado pelo Initializr e o nosso StatusControllerTest):
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] BUILD SUCCESS
O ciclo de feedback rápido: DevTools no desenvolvimento contínuo
Para manter o fluxo de trabalho ágil durante o desenvolvimento, adicionamos a dependência spring-boot-devtools 7 logo na inicialização do projeto.
O DevTools atua em duas frentes principais:
- Reinicialização Automática (Automatic Restart): O Spring Boot monitora as classes compiladas no classpath. Sempre que você altera um arquivo e salva no editor (ou compila com
./mvnw compile), o DevTools reinicia o contexto da aplicação em frações de segundo, utilizando dois classloaders distintos para reaproveitar as bibliotecas de terceiros e recarregar apenas o código do seu projeto. - Desativação de Caches de Templates: Garante que alterações em recursos estáticos sejam refletidas imediatamente sem necessidade de reiniciar o servidor.
Exercícios
Para fixar a dinâmica de configuração externa, perfis e testes, execute os exercícios abaixo no terminal.
1. Sobrescrita de propriedade via linha de comando
Inicie a aplicação passando uma mensagem personalizada através da variável de ambiente APP_MENSAGEM e, em outro terminal, faça uma requisição HTTP via curl para a rota /api/status validando se o JSON retornado reflete o novo texto.
Ver resposta
# Terminal 1: Iniciar com a variavel customizada
$ APP_MENSAGEM="Mensagem injetada via ENV" ./mvnw spring-boot:run
# Terminal 2: Testar o endpoint
$ curl -s http://localhost:8080/api/status
{"status":"UP","mensagem":"Mensagem injetada via ENV","versao":"1.0.0"}
O Spring Boot mapeia automaticamente variaveis de ambiente em letras maiusculas e com underscores para as chaves correspondentes do YAML.
2. Execução de testes unitários filtrados no Maven
Em projetos grandes com centenas de testes, rodar a suíte completa a cada pequena alteração pode ser demorado. Utilize a flag -Dtest do Maven para executar exclusivamente a classe StatusControllerTest.
Ver resposta
$ ./mvnw test -Dtest=StatusControllerTest
# Executa apenas os testes da classe especificada
A propriedade -Dtest aceita o nome simples da classe ou padroes com asterisco (ex: -Dtest=ControllerTest).*
3. Criando um novo campo no Record e atualizando o teste
Adicione um novo campo booleano boolean manutencao no record StatusResposta, ajuste o StatusController para retornar false por padrão e adicione uma nova asserção no teste unitário StatusControllerTest para validar que o campo $.manutencao vem com o valor esperado.
Ver resposta
No record StatusResposta.java:
package io.github.vndmtrx.tarefas_api;
public record StatusResposta(String status, String mensagem, String versao, boolean manutencao) {}
No StatusController.java:
@GetMapping
public ResponseEntity<StatusResposta> verificarStatus() {
var resposta = new StatusResposta("UP", mensagem, versao, false);
return ResponseEntity.ok(resposta);
}
No teste StatusControllerTest.java, adicione o teste abaixo para validar o novo campo:
@Test
@DisplayName("Deve retornar status 200 com mensagem, versão e manutencao")
void deveRetornarStatusComSucessoComMensagemVersaoEManutencao() throws Exception {
mockMvc.perform(get("/api/status")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"))
.andExpect(jsonPath("$.versao").value("1.0.0"))
.andExpect(jsonPath("$.manutencao").value(false));
}
Execute ./mvnw test para validar a suíte de testes.
O ponto de partida para o domínio
Neste segundo passo da série, estabelecemos as fundações de arquitetura do nosso backend: inicializamos o projeto com Maven, adotamos a filosofia dos 12-Factor App para externalizar configurações no application.yaml, estruturamos o isolamento por perfis e garantimos que todo endpoint nasce acompanhado de testes unitários automatizados.
Com o esqueleto da aplicação de pé e o ciclo de feedback azeitado pelo DevTools, estamos prontos para avançar para as regras de negócio.
Na Parte 3, entraremos no domínio da nossa aplicação de tarefas: vamos modelar a entidade Tarefa, configurar a persistência em memória com H2, estruturar as camadas de Repository, Service e Controller, e introduzir as variáveis de infraestrutura de banco de dados.
Referências
-
Externalized Configuration in Spring Boot {Spring Boot Reference Documentation} (Link) ↩
-
Spring Boot Profiles {Spring Boot Reference Documentation} (Link) ↩
-
Relaxed Binding on Maps in Spring Boot {Spring Boot Reference Documentation} (Link) ↩
-
Developer Tools (DevTools) Guide {Spring Boot Reference Documentation} (Link) ↩