Spring Boot
SLF4J
SLF4J (Simple Logging Facade for Java) is the logging abstraction that Spring Boot uses throughout its codebase. Application code depends on SLF4J's API — Logger and LoggerFactory — while the actual implementation (Logback, Log4j2, or JUL) is determined by what is on the classpath at runtime. This entry covers the SLF4J API, logger hierarchy, bridging legacy logging frameworks, fluent logging API, and how Spring Boot wires SLF4J to Logback.
SLF4J Logger and LoggerFactory
SLF4J provides a Logger interface and a LoggerFactory that returns the correct implementation at runtime. Every class that logs should declare one private static final Logger. The logger name — conventionally the class name — forms part of the logger hierarchy that allows level configuration per package.
Java
// ── Standard SLF4J logger declaration ────────────────────────────────
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class PaymentService {
// ── Convention: logger named after the class ──────────────────────
private static final Logger log =
LoggerFactory.getLogger(PaymentService.class);
// ── Equivalent — uses class name string ───────────────────────────
// private static final Logger log =
// LoggerFactory.getLogger("com.myapp.service.PaymentService");
public PaymentResponse charge(Long orderId,
BigDecimal amount) {
log.debug("Charging {} for order {}", amount, orderId);
try {
PaymentResponse result = gateway.charge(amount);
log.info("Payment successful orderId={} ref={}",
orderId, result.reference());
return result;
} catch (CardDeclinedException ex) {
log.warn("Card declined orderId={} reason={}",
orderId, ex.getMessage());
throw ex;
} catch (Exception ex) {
log.error("Payment failed orderId={}",
orderId, ex);
throw ex;
}
}
}
// ── Lombok @Slf4j — generates the same declaration ────────────────────
@Service
@Slf4j // generates: private static final Logger log = ...
public class OrderService {
public void process(Long orderId) {
log.info("Processing order {}", orderId);
}
}
// ── Logger hierarchy ──────────────────────────────────────────────────
// com.myapp ← parent
// com.myapp.service ← inherits level from parent
// com.myapp.service.OrderService ← most specific
//
// Setting logging.level.com.myapp=DEBUG sets the level for:
// - com.myapp.service.OrderService
// - com.myapp.repository.OrderRepository
// - all other classes under com.myappParameterised Logging and the Fluent API
SLF4J's parameterised logging delays string construction until the log level is enabled. The fluent API (SLF4J 2.0+) builds log events with explicit key-value pairs using atLevel().setMessage().addKeyValue() — cleaner than long parameter lists and compatible with structured logging backends.
Java
@Service
@Slf4j
public class UserService {
// ── Parameterised — preferred ─────────────────────────────────────
public UserResponse findById(Long id) {
log.debug("Loading user {}", id);
User user = userRepo.findById(id).orElseThrow();
log.debug("Loaded user {} email={}",
id, user.getEmail());
return UserResponse.from(user);
}
// ── Fluent API (SLF4J 2.0+) ───────────────────────────────────────
public void register(RegisterRequest request) {
log.atInfo()
.setMessage("Registering user")
.addKeyValue("email", request.email())
.addKeyValue("name", request.name())
.log();
User user = save(request);
log.atInfo()
.setMessage("User registered")
.addKeyValue("userId", user.getId())
.addKeyValue("email", user.getEmail())
.log();
}
// ── Fluent API with exception ─────────────────────────────────────
public void process(Long userId) {
try {
doProcess(userId);
} catch (Exception ex) {
log.atError()
.setMessage("Processing failed")
.addKeyValue("userId", userId)
.setCause(ex)
.log();
throw ex;
}
}
// ── Level check for expensive argument construction ───────────────
public void audit(List<Order> orders) {
// isDebugEnabled() prevents building the list string
if (log.isDebugEnabled()) {
log.debug("Auditing orders: {}",
orders.stream()
.map(o -> o.getId().toString())
.collect(Collectors.joining(",")));
}
// Fluent API equivalent — lazy argument evaluation
log.atDebug()
.setMessage("Auditing {} orders")
.addArgument(orders::size) // supplier — lazy
.log();
}
}MDC — Mapped Diagnostic Context
SLF4J's MDC is a thread-local map of key-value pairs that is automatically included in every log statement on that thread. It is the standard mechanism for propagating context (correlation ID, user ID, tenant ID) through the call stack without passing it as method parameters.
Java
// ── MDC operations ────────────────────────────────────────────────────
import org.slf4j.MDC;
// Set a value
MDC.put("correlationId", "abc-123");
MDC.put("userId", "42");
// Get a value
String correlationId = MDC.get("correlationId");
// Remove a value
MDC.remove("correlationId");
// Clear all values (ALWAYS do this on thread pool threads)
MDC.clear();
// Copy current MDC (for passing to a new thread)
Map<String, String> snapshot = MDC.getCopyOfContextMap();
// Restore a copied MDC on a different thread
MDC.setContextMap(snapshot);
// ── try-with-resources — auto-close restores previous value ──────────
try (MDC.MDCCloseable ignored =
MDC.putCloseable("requestId", requestId)) {
// MDC.get("requestId") returns requestId here
log.info("Processing request");
}
// MDC.get("requestId") is null here — automatically removed
// ── Service layer MDC usage ───────────────────────────────────────────
@Service
@Slf4j
public class InvoiceService {
public InvoiceResponse generate(Long orderId) {
try (MDC.MDCCloseable orderId_ =
MDC.putCloseable("orderId",
String.valueOf(orderId))) {
log.info("Generating invoice"); // includes orderId
validate(orderId);
log.debug("Validation passed"); // includes orderId
Invoice invoice = buildInvoice(orderId);
log.info("Invoice {} generated", // includes orderId
invoice.getNumber());
return InvoiceResponse.from(invoice);
}
}
}
// ── Propagate MDC to @Async threads ──────────────────────────────────
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("mdcAwareExecutor")
public Executor mdcAwareExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.initialize();
return new TaskDecorator() {
// Wrap each task to copy MDC from the submitting thread
// to the worker thread
public Runnable decorate(Runnable r) {
Map<String, String> ctx =
MDC.getCopyOfContextMap();
return () -> {
if (ctx != null) MDC.setContextMap(ctx);
try { r.run(); }
finally { MDC.clear(); }
};
}
};
}
}Bridging Legacy Logging Frameworks
Many libraries use java.util.logging (JUL), Apache Commons Logging (JCL), or Log4j 1.x directly. SLF4J provides bridge JARs that intercept calls to these APIs and route them through SLF4J to the single configured implementation. Spring Boot includes the required bridges automatically.
XML
<!-- Spring Boot starter includes these bridges automatically:
- jul-to-slf4j : java.util.logging → SLF4J
- jcl-over-slf4j : Apache Commons Logging → SLF4J
- log4j-over-slf4j : Log4j 1.x → SLF4J
These are bundled in spring-boot-starter-logging -->
// ── Verify bridges are active ──────────────────────────────────────────
// spring-boot-starter-logging dependency tree:
// └── ch.qos.logback:logback-classic (implementation)
// ├── org.slf4j:slf4j-api (facade)
// └── org.slf4j:jul-to-slf4j (JUL bridge)
// └── org.slf4j:jcl-over-slf4j (JCL bridge)
// └── org.slf4j:log4j-over-slf4j (Log4j 1.x bridge)
// ── Install JUL bridge programmatically (for tests) ───────────────────
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// Install JUL-to-SLF4J bridge before Spring starts
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
SpringApplication.run(Application.class, args);
}
}
// ── Exclude conflicting logging JARs ─────────────────────────────────
<!-- If a dependency brings in log4j-over-slf4j AND logback,
exclude the conflicting JAR: -->
<dependency>
<groupId>some.library</groupId>
<artifactId>some-artifact</artifactId>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
# ── application.yml — suppress noisy third-party loggers ─────────────
logging:
level:
# Apache HttpClient — very verbose at DEBUG
org.apache.http: WARN
# Hibernate SQL detail — only for debugging
org.hibernate.type.descriptor.sql.BasicBinder: WARN
# Spring Data repository query generation
org.springframework.data.jpa.repository.query: WARN
# AWS SDK — chatty
software.amazon.awssdk: WARN
com.amazonaws: WARNSLF4J in Spring Boot Auto-Configuration
Spring Boot's auto-configuration wires SLF4J to Logback through LoggingApplicationListener and LogbackLoggingSystem. The system reads logging.* properties from application.yml before the ApplicationContext starts, making logging available even during context initialisation. Understanding the startup sequence helps diagnose early-startup logging problems.
yaml
// ── Spring Boot logging initialisation sequence ──────────────────────
// 1. JVM starts, Logback initialises with default configuration
// 2. SpringApplication created
// 3. LoggingApplicationListener fires on ApplicationStartingEvent
// 4. LogbackLoggingSystem reads spring.log.* from bootstrap context
// 5. Full ApplicationContext starts — logging.* from application.yml
// 6. LoggingApplicationListener fires on ApplicationEnvironmentPreparedEvent
// 7. Final log levels applied from application.yml
// ── Application log level programmatic override ───────────────────────
@RestController
@RequestMapping("/api/v1/admin/logging")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class LogLevelController {
// Spring Boot exposes this through /actuator/loggers
// but this shows the programmatic approach:
@PostMapping("/{loggerName}")
public ResponseEntity<Void> setLevel(
@PathVariable String loggerName,
@RequestParam String level) {
LoggerContext loggerContext = (LoggerContext)
LoggerFactory.getILoggerFactory();
ch.qos.logback.classic.Logger logger =
loggerContext.getLogger(loggerName);
logger.setLevel(
ch.qos.logback.classic.Level.valueOf(
level.toUpperCase()));
return ResponseEntity.noContent().build();
}
}
# ── Actuator loggers endpoint (built-in) ─────────────────────────────
# GET /actuator/loggers → list all loggers and levels
# GET /actuator/loggers/{name} → get level for one logger
# POST /actuator/loggers/{name} → set level at runtime
# Body: { "configuredLevel": "DEBUG" }
#
management:
endpoints:
web:
exposure:
include: loggers
endpoint:
loggers:
enabled: true
# ── Example: enable DEBUG for a package at runtime (no restart) ───────
# curl -X POST http://localhost:8080/actuator/loggers/com.myapp.service \
# -H "Content-Type: application/json" \
# -d '{"configuredLevel": "DEBUG"}'