spring-projects / spring-projects/spring-boot
SpringApplication startup failure report silently discarded when commons-logging resolves to Jdk14Logger (regression from 3.5.x)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 81.5k
- Forks
- 42.7k
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 65
Description
When the application context fails to refresh (e.g. a BeanCreationException), Spring Boot 4.0.x silently discards the entire failure report — no stacktrace, no FailureAnalyzer output, no Application run failed line — if org.apache.commons.logging.LogFactory resolves to Jdk14Logger rather than the SLF4J adapter. The process exits with code 1 after a single WARN from AnnotationConfigApplicationContext. The same setup prints the full stacktrace correctly in 3.5.x.
Affected versions
- Reproduces: Spring Boot 4.0.6 (likely all 4.0.x)
- Works: Spring Boot 3.5.14
Reproducer
https://github.com/cziberpv/spring-boot-silent-failure-mre
git clone https://github.com/cziberpv/spring-boot-silent-failure-mre.git
cd spring-boot-silent-failure-mre
mvn -DskipTests package
java -cp "target/classes:target/lib/*" com.example.MreApplication
(Windows: replace : with ; in the classpath.)
The reproducer is a spring-boot-starter app with one @Service whose constructor throws. The single deviation from defaults is an explicit commons-logging:1.2 dependency in pom.xml — a realistic situation in enterprise migrations, where legacy libraries (Saperion, httpclient 4.x, JCIFS, custom SOAP/ORM stacks, etc.) pull in commons-logging:1.2 transitively and Maven's nearest-wins resolution lets it replace the 1.3.x copy that Spring Boot 4 expects.
Commenting out that single dependency in pom.xml and rebuilding makes the bug disappear — see expected.txt vs actual.txt in the repo.
Expected behavior
ERROR --- o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'failingService' ...
at ...
Caused by: java.lang.RuntimeException: Simulated startup failure ...
at com.example.FailingService.<init>(FailingService.java:8)
Actual behavior
2026-05-12T... INFO ... Starting MreApplication ...
2026-05-12T... INFO ... No active profile set ...
2026-05-12T... WARN ... Exception encountered during context initialization
- cancelling refresh attempt: ... Constructor threw exception
[process exits with code 1, nothing else printed]
Root cause
Spring Boot 4.0.x replaced spring-jcl (which hard-coded SLF4J and ignored JCL discovery) with apache commons-logging:1.3.x. Apache commons-logging performs runtime discovery whose result depends on which commons-logging jar is loaded by the classloader:
commons-logging:1.2ships only Avalon / JDK13 / JDK14 / Log4j / LogKit / Simple log adapters. No SLF4J adapter exists in the artifact. With no JCL discovery overrides on the classpath, the defaultLogFactoryImplresolves toJdk14Logger.commons-logging:1.3.xaddedSlf4jLogFactoryand auto-selects it when SLF4J is on the classpath — this is what makes Spring Boot 4 work in the default configuration.
When commons-logging:1.2 ends up earlier on the classpath (or replaces 1.3.x via Maven nearest-wins), LogFactory is loaded from 1.2 and the SLF4J integration shipped in 1.3.x is unreachable. From that point on, every LogFactory.getLog(...) call in Spring Framework / Spring Boot returns a Jdk14Logger.
The silent failure on top of that is in Spring Boot itself:
AbstractApplicationContext.refresh()fails and logs a WARN without stacktrace (string concatenation, notlogger.warn(msg, ex)).SpringApplication.handleRunFailure()callslisteners.failed(...)first →LoggingApplicationListener.onApplicationFailedEvent()→cleanupLoggingSystem()→LogbackLoggingSystem.cleanUp()→removeJdkLoggingBridgeHandler(). TheSLF4JBridgeHandleris removed from the JUL root logger; the original JULConsoleHandlerwas already removed when the bridge was installed. The JUL root logger now has zero handlers.handleRunFailure()then callsreportFailure()in itsfinallyblock.LoggingFailureAnalysisReporter.report()(or the fallbacklogger.error("Application run failed", failure)) goes throughJdk14Logger→java.util.logging.Logger→ no handlers → silently discarded.SpringBootExceptionHandler.uncaughtException()sees the exception is registered as "logged", does not delegate to the parent handler, andSystem.exit(1)is called.
In 3.5.x, spring-jcl does not honor JCL discovery — LogFactory.getLog(...) always returns an SLF4J-backed logger, the failure log is routed through Logback's ConsoleAppender, and the stacktrace is printed normally.
Verified via JDWP
On the original deployment the reporter attached a debugger and confirmed:
- Breakpoint at
SpringApplication.java:854(if (logger.isErrorEnabled())) — hit logger.isErrorEnabled()— returnstruelogger.error("Application run failed", failure)— executesloggerruntime type —Jdk14Logger(not SLF4J)java.util.logging.Logger.getLogger("").getHandlers()— empty array- The
logger.error(...)call writes to JUL with no handlers → output is discarded
Why fat-jar / IDE do NOT reproduce
java -jar app.jar(Spring Boot'sJarLauncherwith the nestedBOOT-INF/lib/...layout) iterates jars in the order recorded inBOOT-INF/classpath.idx, wherecommons-logging:1.3.xprecedes any older copy. The explodedlib/*layout used in production deployments expands alphabetically via the JVM glob, putting1.2first.- IntelliJ IDEA constructs the launch classpath from the resolved Maven model, which deduplicates artifacts before launching, so the duplicate
commons-logging:1.2never appears at runtime.
This means the bug is invisible during local java -jar runs and IDE runs, and only manifests in production-style classpath layouts (Maven Assembly, custom distribution, install4j, etc.) — making it particularly hard to diagnose.
Suggested fix
The fix is independent of how JCL ends up resolving to Jdk14Logger — the underlying problem is that cleanupLoggingSystem() removes JUL handlers before reportFailure() writes the final error, and any JUL-routed log call after cleanup is silently lost.
Three options, in increasing order of invasiveness (full discussion in the README of the reproducer):
- Reorder in
SpringApplication.handleRunFailure(): callreportFailure()beforelisteners.failed(context, exception), so the failure report is logged while the logging system is still alive. Most targeted. - Restore a JUL
ConsoleHandlerinAbstractLoggingSystem.cleanUp()after removing the SLF4J bridge handler, so any JUL-routed log call can still reach stderr. Fixes the broader class of "anything logged after cleanup is lost". System.errsafety net inSpringApplication.reportFailure()— write toSystem.errafter the normal logger path, conditional oncleanupLoggingSystem()having already run. Most defensive.
Environment
- Java 21.0.7 (Amazon Corretto)
- Spring Boot 4.0.6 (Spring Framework 7.0.7, Spring Boot 4.0.x default Logback)
- Linux and Windows production deployments; reproducer verified on Windows 11 with Maven 3.9.14
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with SpringApplication.handleRunFailure() and reportFailure(), then trace LoggingApplicationListener cleanup through AbstractLoggingSystem and LogbackLoggingSystem. Run the linked reproducer using its exploded lib/* classpath and compare expected.txt with actual.txt. Done means the startup failure report remains visible when commons-logging:1.2 resolves to Jdk14Logger, without regressing normal startup failure logging.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, spring-boot
- Domain
- backend, observability
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100