Commit a5302212 authored by Phillip Webb's avatar Phillip Webb

Ensure startup failures are only logged once

Update SpringApplication so that startup exceptions are only logged
once. A custom UncaughtExceptionHandler is now used when running in
the main thread to suppress errors that have already been logged.

Fixes gh-4423
parent e8b28796
......@@ -42,7 +42,6 @@ class FileWatchingFailureHandler implements FailureHandler {
@Override
public Outcome handle(Throwable failure) {
failure.printStackTrace();
CountDownLatch latch = new CountDownLatch(1);
FileSystemWatcher watcher = this.fileSystemWatcherFactory.getFileSystemWatcher();
watcher.addSourceFolders(
......
......@@ -50,6 +50,7 @@ class RestartLauncher extends Thread {
}
catch (Throwable ex) {
this.error = ex;
getUncaughtExceptionHandler().uncaughtException(this, ex);
}
}
......
......@@ -271,10 +271,7 @@ public class Restarter {
return;
}
if (failureHandler.handle(error) == Outcome.ABORT) {
if (error instanceof Exception) {
throw (Exception) error;
}
throw new Exception(error);
return;
}
}
while (true);
......
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
/**
* {@link UncaughtExceptionHandler} to suppress handling already logged exceptions.
*
* @author Phillip Webb
*/
class LoggedExceptionHandler implements UncaughtExceptionHandler {
private static LoggedExceptionHandlerThreadLocal handler = new LoggedExceptionHandlerThreadLocal();
private final UncaughtExceptionHandler parent;
private final List<Throwable> exceptions = new ArrayList<Throwable>();
LoggedExceptionHandler(UncaughtExceptionHandler parent) {
this.parent = parent;
}
public void register(Throwable exception) {
this.exceptions.add(exception);
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!isRegistered(ex) && this.parent != null) {
this.parent.uncaughtException(thread, ex);
}
}
private boolean isRegistered(Throwable ex) {
if (this.exceptions.contains(ex)) {
return true;
}
if (ex instanceof InvocationTargetException) {
return isRegistered(ex.getCause());
}
return false;
}
static LoggedExceptionHandler forCurrentThread() {
return handler.get();
}
/**
* Thread local used to attach and track handlers.
*/
private static class LoggedExceptionHandlerThreadLocal
extends ThreadLocal<LoggedExceptionHandler> {
@Override
protected LoggedExceptionHandler initialValue() {
LoggedExceptionHandler handler = new LoggedExceptionHandler(
Thread.currentThread().getUncaughtExceptionHandler());
Thread.currentThread().setUncaughtExceptionHandler(handler);
return handler;
};
}
}
......@@ -301,17 +301,8 @@ public class SpringApplication {
return context;
}
catch (Throwable ex) {
try {
listeners.finished(context, ex);
this.log.error("Application startup failed", ex);
}
finally {
if (context != null) {
context.close();
}
}
ReflectionUtils.rethrowRuntimeException(ex);
return context;
handleRunFailure(context, listeners, ex);
throw new IllegalStateException(ex);
}
}
......@@ -816,6 +807,42 @@ public class SpringApplication {
protected void afterRefresh(ConfigurableApplicationContext context, String[] args) {
}
private void handleRunFailure(ConfigurableApplicationContext context,
SpringApplicationRunListeners listeners, Throwable exception) {
try {
try {
listeners.finished(context, exception);
}
finally {
if (context != null) {
context.close();
}
}
}
catch (Exception ex) {
this.log.warn("Unable to close ApplicationContext", ex);
}
if (this.log.isErrorEnabled()) {
this.log.error("Application startup failed", exception);
registerLoggedException(exception);
}
ReflectionUtils.rethrowRuntimeException(exception);
}
/**
* Register that the given exception has been logged. By default, if the running in
* the main thread, this method will suppress additional printing of the stacktrace.
* @param exception the exception that was logged
*/
protected void registerLoggedException(Throwable exception) {
Thread currentThread = Thread.currentThread();
if (("main".equals(currentThread.getName())
|| "restartedMain".equals(currentThread.getName()))
&& "main".equals(currentThread.getThreadGroup().getName())) {
LoggedExceptionHandler.forCurrentThread().register(exception);
}
}
/**
* Set a specific main application class that will be used as a log source and to
* obtain version information. By default the main application class will be deduced.
......
......@@ -697,6 +697,25 @@ public class SpringApplicationTests {
.next().getName());
}
@Test
public void failureResultsInSingleStackTrace() throws Exception {
ThreadGroup group = new ThreadGroup("main");
Thread thread = new Thread(group, "main") {
@Override
public void run() {
SpringApplication application = new SpringApplication(
FailingConfig.class);
application.setWebEnvironment(false);
application.run();
};
};
thread.start();
thread.join(6000);
int occurrences = StringUtils.countOccurrencesOf(this.output.toString(),
"Caused by: java.lang.RuntimeException: ExpectedError");
assertThat("Expected single stacktrace", occurrences, equalTo(1));
}
private boolean hasPropertySource(ConfigurableEnvironment environment,
Class<?> propertySourceClass, String name) {
for (PropertySource<?> source : environment.getPropertySources()) {
......@@ -810,6 +829,16 @@ public class SpringApplicationTests {
}
@Configuration
static class FailingConfig {
@Bean
public Object fail() {
throw new RuntimeException("ExpectedError");
}
}
@Configuration
static class CommandLineRunConfig {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment