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
This commit is contained in:
parent
e8b28796d9
commit
a530221213
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue