Spring Boot Essentials

Day-to-day Spring Boot commands and patterns: project setup with Maven/Gradle, profiles, configuration, actuator, testing, and native images.

Project bootstrap

# Generate via Spring Initializr from the CLI
curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.4.0 \
  -d javaVersion=21 \
  -d groupId=com.mjalaf \
  -d artifactId=demo \
  -d name=demo \
  -d packageName=com.mjalaf.demo \
  -d dependencies=web,actuator,data-jpa,validation,security,lombok \
  -o demo.zip
unzip demo.zip -d demo && cd demo

# Or with the Spring Boot CLI (sdk install springboot)
spring init --build=maven --java-version=21 --boot-version=3.4.0 \
  --dependencies=web,actuator,data-jpa demo

Run, build, watch

# Maven
./mvnw spring-boot:run
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
./mvnw clean package                    # produces target/*.jar
./mvnw clean package -DskipTests
java -jar target/demo-0.0.1-SNAPSHOT.jar

# Gradle
./gradlew bootRun
./gradlew bootRun --args='--spring.profiles.active=dev'
./gradlew clean bootJar
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar

# Hot reload (devtools must be on classpath)
#   Maven:  <dependency>org.springframework.boot:spring-boot-devtools</dependency>
#   Gradle: developmentOnly 'org.springframework.boot:spring-boot-devtools'

Dependency management

<!-- Maven: add a starter -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
# Maven helpers
./mvnw dependency:tree
./mvnw versions:display-dependency-updates
./mvnw versions:display-plugin-updates

# Gradle helpers
./gradlew dependencies --configuration runtimeClasspath
./gradlew dependencyUpdates       # via com.github.ben-manes.versions plugin

Profiles & configuration

# application.yml — base config
spring:
  application:
    name: demo
  datasource:
    url: jdbc:postgresql://localhost:5432/demo
    username: demo
  jpa:
    hibernate:
      ddl-auto: validate
    open-in-view: false

server:
  port: 8080
  forward-headers-strategy: framework

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      probes:
        enabled: true                 # /actuator/health/liveness + /readiness
# application-dev.yml — activated when SPRING_PROFILES_ACTIVE=dev
spring:
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
logging:
  level:
    org.springframework.web: DEBUG
# Activate profile at runtime
java -jar app.jar --spring.profiles.active=prod
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

Property precedence (highest first): command-line args → SPRING_APPLICATION_JSON → OS env vars → application-<profile>.ymlapplication.yml@PropertySource → defaults.

Minimal REST controller

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor                // Lombok
public class UserController {
    private final UserService service;

    @GetMapping("/{id}")
    public ResponseEntity<UserDto> get(@PathVariable Long id) {
        return service.findById(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@Valid @RequestBody CreateUserRequest req) {
        return service.create(req);
    }
}

Actuator — production observability

# Common endpoints (after enabling them in application.yml)
curl http://localhost:8080/actuator/health
curl http://localhost:8080/actuator/health/liveness
curl http://localhost:8080/actuator/health/readiness
curl http://localhost:8080/actuator/info
curl http://localhost:8080/actuator/metrics
curl http://localhost:8080/actuator/metrics/jvm.memory.used
curl http://localhost:8080/actuator/prometheus   # scrape endpoint
curl http://localhost:8080/actuator/env
curl http://localhost:8080/actuator/loggers
# Change log level at runtime (no restart)
curl -X POST -H "Content-Type: application/json" \
  -d '{"configuredLevel":"DEBUG"}' \
  http://localhost:8080/actuator/loggers/com.mjalaf.demo

Testing

./mvnw test
./mvnw test -Dtest=UserControllerTest
./mvnw verify                            # runs integration tests in failsafe
./gradlew test --tests "*UserControllerTest"
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@Testcontainers
class UserApiIT {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry r) {
        r.add("spring.datasource.url",      postgres::getJdbcUrl);
        r.add("spring.datasource.username", postgres::getUsername);
        r.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired MockMvc mvc;

    @Test
    void healthIsUp() throws Exception {
        mvc.perform(get("/actuator/health"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.status").value("UP"));
    }
}

Docker & containers

# Spring Boot can build an OCI image with no Dockerfile (Cloud Native Buildpacks)
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=mjalaf/demo:0.0.1
./gradlew bootBuildImage --imageName=mjalaf/demo:0.0.1

docker run -p 8080:8080 mjalaf/demo:0.0.1
# Multi-stage Dockerfile (classic approach)
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q -DskipTests package

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]

GraalVM native image (Spring Boot 3+)

# Requires GraalVM 21+. Add the native-maven-plugin (auto-configured by Spring Initializr).
./mvnw -Pnative native:compile
./target/demo                        # ~50 MB binary, ~50 ms startup

# Native container image
./mvnw spring-boot:build-image -Pnative

Database migrations

# Flyway (recommended for SQL)
src/main/resources/db/migration/V1__init.sql
src/main/resources/db/migration/V2__add_users.sql

./mvnw flyway:migrate
./mvnw flyway:info
./mvnw flyway:repair

# Liquibase
src/main/resources/db/changelog/db.changelog-master.yaml
./mvnw liquibase:update
./mvnw liquibase:rollback -Dliquibase.rollbackCount=1

Useful command-line overrides

# Bind to all interfaces, change port, set datasource via env
SERVER_PORT=9090 \
SERVER_ADDRESS=0.0.0.0 \
SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/demo \
java -jar app.jar

# Increase log verbosity for one package
java -jar app.jar --logging.level.org.hibernate.SQL=DEBUG

# Generate a thread dump on a running JVM
jcmd <PID> Thread.print > thread-dump.txt

# Heap dump on OutOfMemoryError
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof -jar app.jar

Spring Boot versions at a glance

VersionReleasedJava baselineNotes
2.7.xMay 2022Java 8Last 2.x line (EOL community; commercial support via Broadcom/Tanzu)
3.0.xNov 2022Java 17Jakarta EE 10 namespace (javax.*jakarta.*), GraalVM native GA
3.1.xMay 2023Java 17Docker Compose support, Testcontainers @ServiceConnection
3.2.xNov 2023Java 17Virtual threads (Loom) support, RestClient
3.3.xMay 2024Java 17CDS support, structured logging
3.4.xNov 2024Java 17Current GA; improved native image + observability

Migrating from 2.x to 3.x: switch to Java 17+, rename javax.*jakarta.*, and re-validate dependencies. Use Spring Boot Migrator or OpenRewrite recipes.