DEV Community

Silver_dev
Silver_dev

Posted on

Spring Boot Under the Hood. Part 9: Actuator, DevTools and the Rest of the Toolbox

This final part gathers the tools that aren't part of the application's core runtime, yet no production project can do without: monitoring, logging, development acceleration, testing, packaging, and native compilation.

9.1. Actuator — Production-Ready Monitoring

Actuator adds ready-made HTTP/JMX endpoints for monitoring and management to your application.

9.1.1. Adding it

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

By default, only /actuator/health and /actuator/info are exposed. The rest are enabled via management.endpoints.web.exposure.include:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,env,beans
Enter fullscreen mode Exit fullscreen mode

9.1.2. Built-in Endpoints

ID What it shows
health Application status (UP/DOWN)
info Arbitrary info (info.* properties)
metrics Micrometer metrics (JVM, HTTP, DataSource)
env All property sources
beans The full list of beans
mappings All @RequestMapping paths
configprops All @ConfigurationProperties
threaddump JVM thread dump
heapdump Heap dump (hprof)
loggers View/change logging levels
shutdown Graceful shutdown — removed in Boot 3.4; use server.shutdown=graceful instead

9.1.3. Your Own @Endpoint

@Component
@Endpoint(id = "featureFlags")
public class FeatureFlagsEndpoint {

    @ReadOperation
    public Map<String, Boolean> getFlags() {
        return Map.of("newCheckout", true, "darkMode", false);
    }

    @WriteOperation
    public void setFlag(@Selector String name, boolean value) {
        // set the flag
    }
}
Enter fullscreen mode Exit fullscreen mode
  • @ReadOperation → HTTP GET (available at http://localhost:8080/actuator/featureFlags)
  • @WriteOperation → HTTP POST
  • @DeleteOperation → HTTP DELETE

9.1.4. HealthIndicators

@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        if (isServiceUp()) {
            return Health.up().withDetail("responseTime", "45ms").build();
        }
        return Health.down().withDetail("error", "Connection refused").build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Actuator collects all HealthIndicator beans and aggregates them into the overall /actuator/health status. The possible statuses are UP, DOWN, OUT_OF_SERVICE, and UNKNOWN. The overall status is the "worst" of them all.

9.1.5. Micrometer

Micrometer is a metrics facade. Actuator automatically registers a MeterRegistry and collects JVM metrics (heap, threads, GC).

@Component
public class OrderMetrics {
    private final Counter orderCounter;
    private final Timer orderTimer;

    public OrderMetrics(MeterRegistry registry) {
        this.orderCounter = Counter.builder("orders.created")
            .description("Number of created orders")
            .register(registry);
        this.orderTimer = Timer.builder("orders.processing.time")
            .register(registry);
    }

    public void onOrderCreated() { orderCounter.increment(); }
    public void recordTime(Runnable task) { orderTimer.record(task); }
}
Enter fullscreen mode Exit fullscreen mode

Prometheus integration:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

The /actuator/prometheus endpoint serves metrics in the Prometheus format.

9.1.6. Boot 4: Actuator Changes

  • Packages relocated: org.springframework.boot.actuate.health.Health → org.springframework.boot.health.contributor.Health.
  • Modularization: Actuator is split into many modules (actuator-web, actuator-jmx, actuator-sbom, etc.).
  • /actuator/info has been extended: process information is now included.

9.2. Logging

9.2.1. The Defaults

Spring Boot uses SLF4J as the facade and Logback as the implementation. At startup, LoggingApplicationListener (registered in spring.factories) initializes logging before the ApplicationContext is even created.

9.2.2. Configuration

System File
Logback logback-spring.xml (recommended), logback.xml
Log4j2 log4j2-spring.xml, log4j2.xml
JUL logging.properties

The -spring variants are recommended: they support <springProfile> and <springProperty>.

<!-- logback-spring.xml -->
<configuration>
    <springProfile name="dev">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>

    <springProfile name="prod">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${LOG_FILE}</file>
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern>
            </encoder>
        </appender>
    </springProfile>
</configuration>
Enter fullscreen mode Exit fullscreen mode

9.2.3. Levels via Properties

logging:
  level:
    root: INFO
    com.example.myapp: DEBUG
    org.springframework.web: WARN
Enter fullscreen mode Exit fullscreen mode

9.2.4. Structured Logging (Boot 3.4+)

Spring Boot 3.4 added native structured logging support with no extra dependencies:

logging:
  structured:
    format:
      console: ecs   # Elastic Common Schema
      file: logstash
Enter fullscreen mode Exit fullscreen mode

Formats: ecs, logstash, gelf.

9.2.5. Boot 4: Changes

  • The default encoding is UTF-8 for Logback, matching Log4j2.
  • No automatic trace/span ID correlation — you need to configure the appender manually.
  • Logback has been upgraded to a new version; some appenders were removed.

9.3. DevTools — Speeding Up Development

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>
Enter fullscreen mode Exit fullscreen mode

9.3.1. Automatic Restart

DevTools uses two ClassLoaders:

ClassLoader What it loads
Base ClassLoader Immutable JARs (Spring, third-party libraries)
Restart ClassLoader Your application's classes (the frequently changing ones)

When a classpath file changes, DevTools re-creates only the Restart ClassLoader while the Base one stays. That's what makes the restart several times faster than a full one.

9.3.2. LiveReload

The built-in LiveReload server automatically refreshes the browser when resources change (HTML, CSS, JS). Requires a LiveReload browser extension. Disabled via spring.devtools.livereload.enabled=false.

9.3.3. include/exclude Configuration

# META-INF/spring-devtools.properties
restart.exclude.companycommonlibs=/mycorp-common-[\\w-]+\.jar
restart.include.projectcommon=/mycorp-myproj-[\\w-]+\.jar
Enter fullscreen mode Exit fullscreen mode

exclude — JARs loaded by the Base ClassLoader. include — the opposite, loaded by the Restart ClassLoader.

9.3.4. Global Settings

A .spring-boot-devtools.properties file in $HOME applies to all projects.

Important: DevTools must never make it into production. optional=true plus Maven/Gradle excludes it from the final JAR.

9.4. Testing

9.4.1. @SpringBootTest — the Full Context

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyApplicationTests {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void contextLoads() {
        ResponseEntity<String> response = restTemplate.getForEntity("/api/hello", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}
Enter fullscreen mode Exit fullscreen mode

webEnvironment:

  • MOCK (default) — no real server, MockMvc
  • RANDOM_PORT — a real server on a random port
  • DEFINED_PORT — on the configured port
  • NONE — no web environment

9.4.2. Slice Tests — Narrow Contexts

Annotation What it loads What to mock
@WebMvcTest Only MVC: controllers, @ControllerAdvice, filters Services via @MockitoBean
@DataJpaTest Only JPA: repositories, @Entity, DataSource Usually H2 in-memory
@JsonTest Only Jackson/Gson —
@RestClientTest Only the REST client (RestTemplate, WebClient) The server via MockWebServer
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private UserService userService;

    @Test
    void shouldReturnUser() throws Exception {
        when(userService.findById(1L)).thenReturn(new User(1L, "Alice"));

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Alice"));
    }
}
Enter fullscreen mode Exit fullscreen mode

The key advantage of slice tests: the context loads faster because only the relevant slice of auto-configuration is enabled.

9.4.3. @MockitoBean — Replacing @MockBean in Boot 4

A critical breaking change in Boot 4:

// Boot 3.x (deprecated in 3.4, removed in 4.0)
@MockBean
private UserService userService;

// Boot 4.x
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@MockitoBean
private UserService userService;
Enter fullscreen mode Exit fullscreen mode

@MockBean and @SpyBean are gone in Boot 4. The replacements are @MockitoBean and @MockitoSpyBean from Spring Framework.

9.4.4. ApplicationContextRunner — Testing Auto-configurations

For testing your own auto-configurations:

class MyAutoConfigurationTests {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class))
        .withPropertyValues("my.service.endpoint=http://localhost:9090");

    @Test
    void shouldCreateMyService() {
        contextRunner.run(context -> {
            assertThat(context).hasSingleBean(MyService.class);
        });
    }

    @Test
    void shouldBackOffWhenCustomBeanPresent() {
        contextRunner
            .withUserConfiguration(CustomConfig.class)
            .run(context -> {
                assertThat(context).hasSingleBean(MyService.class);
                assertThat(context.getBean(MyService.class)).isInstanceOf(CustomMyService.class);
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

ApplicationContextRunner lets you test auto-configuration conditions in isolation, without loading the whole application.

9.5. Build and Packaging

9.5.1. spring-boot-maven-plugin

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
Enter fullscreen mode Exit fullscreen mode

The repackage goal creates an executable JAR (a fat jar):

mvn clean package
java -jar target/myapp-0.0.1-SNAPSHOT.jar
Enter fullscreen mode Exit fullscreen mode

9.5.2. Layered JAR

Since Boot 2.3, the JAR can be split into layers for efficient Docker caching:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layers>
            <enabled>true</enabled>
        </layers>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Layers (from rarely changing to frequently changing):

  1. dependencies — stable dependencies
  2. spring-boot-loader — the loader
  3. snapshot-dependencies — snapshots
  4. application — your code

Dockerfile:

FROM eclipse-temurin:21-jre AS builder
WORKDIR /app
COPY target/myapp-*.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --launcher

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Enter fullscreen mode Exit fullscreen mode

9.5.3. JarLauncher

JarLauncher is the fat jar's entry point. It lives in spring-boot-loader and is responsible for:

  1. Reading MANIFEST.MF (Main-Class: org.springframework.boot.loader.launch.JarLauncher)
  2. Building a URLClassLoader with all the nested JARs
  3. Delegating to the Start-Class (your main class)

There's also WarLauncher and PropertiesLauncher (for external configurations).

9.5.4. Buildpacks

mvn spring-boot:build-image
Enter fullscreen mode Exit fullscreen mode

Spring Boot uses Paketo Buildpacks to create an OCI image without a Dockerfile:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <image>
            <name>myregistry/myapp:${project.version}</name>
        </image>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

9.5.5. Boot 4: Changes

  • Starters renamed: spring-boot-starter-web → spring-boot-starter-webmvc.
  • Undertow removed (incompatible with Servlet 6.1).
  • spring-boot-loader remains, but the packages have been relocated.

9.6. AOT and GraalVM Native Image (an Overview)

9.6.1. What Is AOT

Ahead-of-Time — processing the application at build time rather than at runtime. Spring Boot 3+ performs AOT processing that:

  1. Scans all @Configuration, @Bean, @Component classes.
  2. Generates Java code for bean registration (instead of reflection).
  3. Registers RuntimeHints — hints for GraalVM.

Why: GraalVM Native Image compiles Java into a native executable. Reflection, dynamic proxies, and resource loading only work in a native image if you declare them at build time.

9.6.2. RuntimeHintsRegistrar

public class MyRuntimeHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        hints.reflection().registerType(MyDto.class, MemberCategory.values());
        hints.resources().registerPattern("my-config/*.json");
    }
}
Enter fullscreen mode Exit fullscreen mode

Registration:

@ImportRuntimeHints(MyRuntimeHints.class)
@Configuration
public class MyConfig { }
Enter fullscreen mode Exit fullscreen mode

9.6.3. @RegisterReflectionForBinding — a Shortcut

@Configuration
@RegisterReflectionForBinding({User.class, UserDto.class})
public class JacksonConfig { }
Enter fullscreen mode Exit fullscreen mode

The annotation registers reflection hints for DTOs that get serialized by Jackson.

9.6.4. Building a Native Image

mvn -Pnative native:compile
Enter fullscreen mode Exit fullscreen mode

The result is a native executable:

./target/myapp
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Instant startup (milliseconds instead of seconds)
  • Lower memory consumption
  • Smaller image size

Limitations:

  • No dynamic class loading
  • No JIT (lower peak performance)
  • Longer builds
  • Not all libraries are supported

9.6.5. Boot 4: AOT by Default

In Boot 4, AOT processing is more aggressive: reflection-free by default for most scenarios. BeanRegistrar (introduced in Spring Framework 7) is the primary way to register beans for AOT, replacing reflection-based @Bean methods.

9.7. Summary Table: Boot 3 vs Boot 4

Aspect Spring Boot 3.x Spring Boot 4.x
Actuator packages org.springframework.boot.actuate.health org.springframework.boot.health.contributor
Actuator modules A single spring-boot-actuator Many modules (web, jmx, sbom...)
Default logging Logback, charset depends on the OS Logback, UTF-8 always
Structured logging Introduced in 3.4 Fully integrated
Test annotations @MockBean, @SpyBean Removed; replaced by @MockitoBean, @MockitoSpyBean
Starters spring-boot-starter-web spring-boot-starter-webmvc
AOT Opt-in, -Pnative More aggressive, reflection-free
Buildpacks Supported Supported, improved
Layered JAR Present Present, improved

9.8. Key Takeaways

  1. Actuator — not just /health. Custom endpoints via @Endpoint + @ReadOperation/@WriteOperation. HealthIndicator — for checking external services. Micrometer — for metrics.
  2. Logging is initialized before the ApplicationContext. Use logback-spring.xml, not logback.xml. Since Boot 3.4 — structured logging out of the box.
  3. DevTools uses two ClassLoaders: Base (stable JARs) and Restart (your code). It restarts only the latter.
  4. Testing: @SpringBootTest — the full context; slice tests — narrow contexts. ApplicationContextRunner — for testing auto-configurations.
  5. @MockBean → @MockitoBean — a critical breaking change in Boot 4.
  6. Layered JAR — splitting into layers for Docker caching. JarLauncher — the fat jar's entry point.
  7. Buildpacks — an OCI image without a Dockerfile: mvn spring-boot:build-image.
  8. AOT — build-time processing for GraalVM. @RegisterReflectionForBinding — for DTOs. Native image: instant startup, but with limitations.

The End of the Series

Top comments (0)