O que é Spring Boot?
O Spring Boot é uma camada sobre o Spring Framework que elimina a configuração que o Spring exigia antigamente. Em vez de arquivos XML, configuração de servlets e a necessidade de um container para deploy, você escreve uma classe com um método main, adiciona uma dependência starter e obtém uma aplicação em execução com um servidor embarcado.
Três conceitos fazem o trabalho pesado. Os Starters agrupam as dependências de uma funcionalidade em apenas uma linha. A Auto-configuration inspeciona o classpath e configura beans com base em padrões sensatos. O servidor embarcado significa que a aplicação é um processo Java normal — não há nada para instalar ou onde fazer o deploy.
O resultado é que o Spring Boot se tornou a maneira padrão de construir serviços JVM, desde pequenas APIs internas até grandes frotas de microserviços, mantendo todo o poder do ecossistema Spring por baixo.
O ecossistema Spring em um minuto
O Spring não é apenas uma biblioteca, mas sim uma família que compartilha o mesmo modelo de programação:
- Spring Framework — o container core, injeção de dependência e a camada web.
- Spring Data — repositórios para bancos SQL e NoSQL.
- Spring Security — autenticação e autorização.
- Spring Batch / Integration — processamento em lote e mensageria.
- Spring Cloud — configuração, discovery e resiliência para sistemas distribuídos.
Como foram projetados em conjunto, um projeto pode adotá-los gradualmente sem precisar alterar a forma como o código é escrito. Essa consistência é o real motivo pelo qual as equipes padronizam seus projetos com Spring.
Starters e gerenciamento de dependências
Um starter é um conjunto curado de dependências que funcionam juntas. Você declara a funcionalidade, e não os jars individuais.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
O equivalente no Gradle é igualmente curto:
dependencies {
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-validation"
runtimeOnly "org.postgresql:postgresql"
}
O POM pai ou o plugin de dependency-management define as versões compatíveis para você. Sobrescreva uma versão apenas quando houver um motivo, e deixe a plataforma gerenciar o restante.
A classe da aplicação e o servidor embarcado
Toda aplicação Spring Boot começa aqui. Esta única classe é suficiente para iniciar um servidor web.
package com.example.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
@SpringBootApplication combina três anotações: @Configuration, @EnableAutoConfiguration e @ComponentScan. O escaneamento de componentes (component scanning) começa no pacote desta classe e segue para baixo, por isso a classe principal deve ficar na raiz da estrutura de pacotes do seu projeto.
Executar ./mvnw spring-boot:run inicia um Tomcat embarcado na porta 8080. Você pode trocá-lo por Jetty ou Netty apenas alterando uma dependência, e o código da aplicação permanece inalterado.
A camada web anotada
O Spring MVC mapeia requisições HTTP para métodos utilizando anotações. @RestController combina @Controller e @ResponseBody, fazendo com que os objetos retornados sejam serializados para JSON pelo Jackson.
@RestController
@RequestMapping("/api/posts")
public class PostController {
private final PostService service;
public PostController(PostService service) {
this.service = service;
}
@GetMapping
public List<PostResponse> list(
@RequestParam(defaultValue = "0") int page) {
return service.findPage(page);
}
@GetMapping("/{id}")
public PostResponse get(@PathVariable Long id) {
return service.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PostResponse create(@Valid @RequestBody CreatePostRequest request) {
return service.create(request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
}
@PathVariable lê da URL, @RequestParam da query string, e @RequestBody desserializa o payload. Mantenha os controllers enxutos: eles devem traduzir o HTTP em uma chamada de serviço e traduzir o resultado de volta. A lógica de domínio pertence à camada de serviço.
Injeção de dependência e o contexto da aplicação
O application context é o container que cria objetos, chamados de beans, e fornece suas dependências. Você declara o que uma classe precisa; o Spring decide como construí-la.
@Service
public class PostService {
private final PostRepository repository;
private final Clock clock;
public PostService(PostRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
@Transactional
public PostResponse create(CreatePostRequest request) {
var post = new Post(request.title(), request.body(), clock.instant());
return PostResponse.from(repository.save(post));
}
}
Anotações de estereótipo marcam as classes a serem gerenciadas: @Component para beans genéricos, @Service para serviços de domínio, @Repository para acesso a dados e @RestController para endpoints web. Para qualquer coisa que você não possa anotar — como uma classe de terceiros ou um cliente configurado — declare-a em uma classe @Configuration.
@Configuration
class ClockConfig {
@Bean
Clock clock() {
return Clock.systemUTC();
}
}
Prefira a injeção via construtor em todos os lugares. Isso torna as dependências explícitas, permite o uso de campos final e permite que você instancie a classe em um teste unitário simples sem precisar iniciar o Spring.
Spring Data JPA e o banco de dados
As entidades mapeiam objetos Java para tabelas, e os repositórios fornecem acesso aos dados sem a necessidade de uma implementação.
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 120)
private String title;
@Column(nullable = false)
private String body;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Author author;
protected Post() {
}
public Post(String title, String body, Author author) {
this.title = title;
this.body = body;
this.author = author;
}
// getters and domain methods
}
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByPublishedTrueOrderByCreatedAtDesc();
@Query("select p from Post p join fetch p.author where p.id = :id")
Optional<Post> findWithAuthor(Long id);
}
O Spring Data deriva uma query a partir do nome do método, portanto findByPublishedTrueOrderByCreatedAtDesc não precisa de corpo. Use @Query quando o nome derivado for ilegível, e join fetch para evitar o problema de N+1 do lazy-loading. Mantenha ddl-auto como validate em produção e gerencie o schema com Flyway ou Liquibase.
Configuração com application.yml e profiles
A configuração reside em src/main/resources e pode ser sobrescrita por variáveis de ambiente, o que permite que o mesmo jar seja executado em qualquer lugar.
# src/main/resources/application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/blog
username: blog
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
server:
port: 8080
Os profiles permitem que a mesma build se comporte de maneira diferente dependendo do ambiente.
# src/main/resources/application-prod.yml
spring:
datasource:
url: ${DATABASE_URL}
logging:
level:
root: warn
Ative um profile com --spring.profiles.active=prod ou através da variável de ambiente SPRING_PROFILES_ACTIVE. @ConfigurationProperties vincula um grupo de configurações a uma classe tipada, o que é mais seguro do que espalhar anotações @Value por todo o código.
Validação
As constraints do Jakarta Bean Validation em um objeto de requisição são aplicadas quando o parâmetro do controller é anotado com @Valid.
public record CreatePostRequest(
@NotBlank @Size(max = 120) String title,
@NotBlank String body
) {
}
Um @RestControllerAdvice centraliza a resposta de erro para que todos os endpoints retornem o mesmo formato.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> onValidation(MethodArgumentNotValidException ex) {
return ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(first, second) -> first));
}
}
Valide na fronteira e permita que o domínio confie em suas entradas. Combinado com um exception handler consistente, isso remove verificações repetitivas de if (input == null) de cada serviço.
Spring Security em resumo
O Spring Security é uma cadeia de filtros (filter chain) posicionada à frente da sua aplicação. No Spring Boot moderno, você o configura através de um bean em vez de XML.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
}
Esse único bean exige um JWT válido em todos os endpoints, exceto no health check. Regras a nível de método com @PreAuthorize adicionam autorização granular onde for necessário. Segurança é um tema complexo — planeje ler a documentação cuidadosamente antes de expor qualquer coisa na internet.
Actuator: saúde e métricas
Adicionar o starter do Actuator expõe endpoints operacionais gratuitamente.
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
curl localhost:8080/actuator/health
/actuator/health integra-se com probes de liveness e readiness, e /actuator/metrics reporta estatísticas de JVM, HTTP e datasource. Exponha apenas o que for necessário e proteja o restante — esses endpoints revelam muitos detalhes sobre um sistema em execução.
Testes
@SpringBootTest inicia o contexto da aplicação e serve como base para testes de integração. MockMvc exercita a camada web sem abrir uma porta real.
@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void listsPosts() throws Exception {
mockMvc.perform(get("/api/posts"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title").value("Hello"));
}
}
Para um feedback mais rápido, fragmente o contexto apenas para a camada sob teste e utilize mocks para os colaboradores.
@WebMvcTest(PostController.class)
class PostControllerSliceTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private PostService service;
@Test
void returnsNotFound() throws Exception {
given(service.findById(1L)).willThrow(new PostNotFoundException(1L));
mockMvc.perform(get("/api/posts/1"))
.andExpect(status().isNotFound());
}
}
Use slices para controllers e repositories, e reserve o contexto completo para alguns testes end-to-end. O Testcontainers fornece um banco de dados real nos testes sem a necessidade de mockar a persistência.
Build e deploy
O build gera um executable fat jar que contém suas classes, dependências e o servidor embarcado.
./mvnw clean package
java -jar target/blog-0.0.1-SNAPSHOT.jar
Para containers, o Buildpacks cria uma imagem otimizada sem a necessidade de um Dockerfile.
./mvnw spring-boot:build-image
A configuração vem do ambiente, portanto, o mesmo artefato move-se de staging para produção sem alterações. Adicione um Dockerfile apenas quando precisar de controles que o Buildpacks não oferece, e prefira um multi-stage build para manter a imagem compacta.
Melhores práticas
- Organize os pacotes por funcionalidade (package by feature) para que cada domínio possua seu próprio controller, service e repository.
- Use injeção via construtor e mantenha os campos
final. - Retorne DTOs nos controllers em vez de entidades JPA.
- Valide os objetos de requisição com
@Valide trate os erros em um único@RestControllerAdvice. - Mantenha o
ddl-autoemvalidatee gerencie as alterações de schema com Flyway ou Liquibase. - Externalize a configuração e utilize profiles em vez de recompilar para cada ambiente.
- Adicione o Actuator, então exponha e proteja apenas os endpoints que você realmente precisa.
Erros comuns
- Injeção de campo com
@Autowired, o que oculta dependências e complica os testes. - Serializar entidades diretamente, expondo lazy-loading ou colunas internas.
- Permitir que os controllers acumulem lógica de negócio em vez de delegá-la para services.
- Usar
ddl-auto: updateem produção, causando divergências em relação ao schema real. - Iniciar todo o contexto em cada teste unitário, tornando a suíte de testes lenta.
- Expor todos os endpoints do Actuator para a internet pública.
- Adicionar starters desnecessários e pagar o preço disso no tempo de startup.
Próximos passos
O Spring Boot ensina injeção de dependência, auto-configuração e design em camadas — conceitos que vão muito além do Java. Se você gostou do modelo de módulos e injeção, o NestJS aplica isso em TypeScript, enquanto o Express mostra a alternativa minimalista. A camada web anotada é, fundamentalmente, uma REST API, e a maioria das aplicações persiste dados em um armazenamento relacional, tornando o PostgreSQL o próximo passo natural.