Allow ExitCodeGenerator to be used on Exceptions

Update exit code support to allow the ExitCodeGenerator interface to
be placed on an Exception. Any uncaught exception implementing the
interface and returning a non `0` status will now trigger a System.exit
with the code.

Fixes gh-4803
This commit is contained in:
Phillip Webb 2016-01-13 11:56:24 +00:00
parent d2fed8bb07
commit 7397dbaf57
10 changed files with 186 additions and 40 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 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.
@ -53,11 +53,11 @@ public class WarCommandIT {
.start();
invocation = new Invocation(process);
invocation.await();
assertThat(invocation.getErrorOutput(), containsString("onStart error"));
assertThat(invocation.getStandardOutput(), containsString("Tomcat started"));
assertThat(invocation.getStandardOutput(),
assertThat(invocation.getOutput(), containsString("onStart error"));
assertThat(invocation.getOutput(), containsString("Tomcat started"));
assertThat(invocation.getOutput(),
containsString("/WEB-INF/lib-provided/tomcat-embed-core"));
assertThat(invocation.getStandardOutput(),
assertThat(invocation.getOutput(),
containsString("/WEB-INF/lib-provided/tomcat-embed-core"));
process.destroy();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 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.
@ -93,21 +93,27 @@ public final class CommandLineInvoker {
private final StringBuffer out = new StringBuffer();
private final StringBuffer combined = new StringBuffer();
private final Process process;
private final List<Thread> streamReaders = new ArrayList<Thread>();
public Invocation(Process process) {
this.process = process;
this.streamReaders.add(new Thread(
new StreamReadingRunnable(this.process.getErrorStream(), this.err)));
this.streamReaders.add(new Thread(
new StreamReadingRunnable(this.process.getInputStream(), this.out)));
this.streamReaders.add(new Thread(new StreamReadingRunnable(
this.process.getErrorStream(), this.err, this.combined)));
this.streamReaders.add(new Thread(new StreamReadingRunnable(
this.process.getInputStream(), this.out, this.combined)));
for (Thread streamReader : this.streamReaders) {
streamReader.start();
}
}
public String getOutput() {
return postProcessLines(getLines(this.combined));
}
public String getErrorOutput() {
return postProcessLines(getLines(this.err));
}
@ -161,13 +167,13 @@ public final class CommandLineInvoker {
private final InputStream stream;
private final StringBuffer output;
private final StringBuffer[] outputs;
private final byte[] buffer = new byte[4096];
private StreamReadingRunnable(InputStream stream, StringBuffer buffer) {
private StreamReadingRunnable(InputStream stream, StringBuffer... outputs) {
this.stream = stream;
this.output = buffer;
this.outputs = outputs;
}
@Override
@ -175,7 +181,9 @@ public final class CommandLineInvoker {
int read;
try {
while ((read = this.stream.read(this.buffer)) > 0) {
this.output.append(new String(this.buffer, 0, read));
for (StringBuffer output : this.outputs) {
output.append(new String(this.buffer, 0, read));
}
}
}
catch (IOException ex) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 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.
@ -42,6 +42,7 @@ public class SampleBatchApplication {
@Bean
protected Tasklet tasklet() {
return new Tasklet() {
@Override
public RepeatStatus execute(StepContribution contribution,
@ -49,6 +50,7 @@ public class SampleBatchApplication {
return RepeatStatus.FINISHED;
}
};
}
@Bean

View File

@ -0,0 +1,28 @@
/*
* Copyright 2012-2016 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 sample.simple;
import org.springframework.boot.ExitCodeGenerator;
public class ExitException extends RuntimeException implements ExitCodeGenerator {
@Override
public int getExitCode() {
return 10;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 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.
@ -37,6 +37,9 @@ public class SampleSimpleApplication implements CommandLineRunner {
@Override
public void run(String... args) {
System.out.println(this.helloWorldService.getHelloMessage());
if (args.length > 0 && args[0].equals("exitcode")) {
throw new ExitException();
}
}
public static void main(String[] args) throws Exception {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2016 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.
@ -16,6 +16,7 @@
package org.springframework.boot.loader;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Method;
/**
@ -53,7 +54,14 @@ public class MainMethodRunner implements Runnable {
mainMethod.invoke(null, new Object[] { this.args });
}
catch (Exception ex) {
ex.printStackTrace();
UncaughtExceptionHandler handler = Thread.currentThread()
.getUncaughtExceptionHandler();
if (handler != null) {
handler.uncaughtException(Thread.currentThread(), ex);
}
else {
ex.printStackTrace();
}
System.exit(1);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2016 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.
@ -18,7 +18,8 @@ package org.springframework.boot;
/**
* Interface used to generate an 'exit code' from a running command line
* {@link SpringApplication}.
* {@link SpringApplication}. Since 1.3.2 this interface can be used on exceptions as well
* as directly on beans.
*
* @author Dave Syer
* @see SpringApplication#exit(org.springframework.context.ApplicationContext,

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 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.
@ -827,6 +827,7 @@ public class SpringApplication {
}
try {
try {
handeExitCode(context, exception);
listeners.finished(context, exception);
}
finally {
@ -847,14 +848,46 @@ public class SpringApplication {
* @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);
SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
if (handler != null) {
handler.registerLoggedException(exception);
}
}
private void handeExitCode(ConfigurableApplicationContext context,
Throwable exception) {
int exitCode = getExitCodeFromException(exception);
if (exitCode != 0) {
SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
if (handler != null) {
handler.registerExitCode(exitCode);
}
}
}
private int getExitCodeFromException(Throwable exception) {
if (exception == null) {
return 0;
}
if (exception instanceof ExitCodeGenerator) {
return ((ExitCodeGenerator) exception).getExitCode();
}
return getExitCodeFromException(exception.getCause());
}
SpringBootExceptionHandler getSpringBootExceptionHandler() {
if (isMainThread(Thread.currentThread())) {
return SpringBootExceptionHandler.forCurrentThread();
}
return null;
}
private boolean isMainThread(Thread currentThread) {
return ("main".equals(currentThread.getName())
|| "restartedMain".equals(currentThread.getName()))
&& "main".equals(currentThread.getThreadGroup().getName());
}
/**
* 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.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 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.
@ -25,11 +25,12 @@ import java.util.List;
import java.util.Set;
/**
* {@link UncaughtExceptionHandler} to suppress handling already logged exceptions.
* {@link UncaughtExceptionHandler} to suppress handling already logged exceptions and
* dealing with system exit.
*
* @author Phillip Webb
*/
class LoggedExceptionHandler implements UncaughtExceptionHandler {
class SpringBootExceptionHandler implements UncaughtExceptionHandler {
private static Set<String> LOG_CONFIGURATION_MESSAGES;
@ -43,14 +44,20 @@ class LoggedExceptionHandler implements UncaughtExceptionHandler {
private final UncaughtExceptionHandler parent;
private final List<Throwable> exceptions = new ArrayList<Throwable>();
private final List<Throwable> loggedExceptions = new ArrayList<Throwable>();
LoggedExceptionHandler(UncaughtExceptionHandler parent) {
private int exitCode = 0;
SpringBootExceptionHandler(UncaughtExceptionHandler parent) {
this.parent = parent;
}
public void register(Throwable exception) {
this.exceptions.add(exception);
public void registerLoggedException(Throwable exception) {
this.loggedExceptions.add(exception);
}
public void registerExitCode(int exitCode) {
this.exitCode = exitCode;
}
@Override
@ -61,7 +68,10 @@ class LoggedExceptionHandler implements UncaughtExceptionHandler {
}
}
finally {
this.exceptions.clear();
this.loggedExceptions.clear();
if (this.exitCode != 0) {
System.exit(this.exitCode);
}
}
}
@ -88,7 +98,7 @@ class LoggedExceptionHandler implements UncaughtExceptionHandler {
}
private boolean isRegistered(Throwable ex) {
if (this.exceptions.contains(ex)) {
if (this.loggedExceptions.contains(ex)) {
return true;
}
if (ex instanceof InvocationTargetException) {
@ -97,7 +107,7 @@ class LoggedExceptionHandler implements UncaughtExceptionHandler {
return false;
}
static LoggedExceptionHandler forCurrentThread() {
static SpringBootExceptionHandler forCurrentThread() {
return handler.get();
}
@ -105,11 +115,11 @@ class LoggedExceptionHandler implements UncaughtExceptionHandler {
* Thread local used to attach and track handlers.
*/
private static class LoggedExceptionHandlerThreadLocal
extends ThreadLocal<LoggedExceptionHandler> {
extends ThreadLocal<SpringBootExceptionHandler> {
@Override
protected LoggedExceptionHandler initialValue() {
LoggedExceptionHandler handler = new LoggedExceptionHandler(
protected SpringBootExceptionHandler initialValue() {
SpringBootExceptionHandler handler = new SpringBootExceptionHandler(
Thread.currentThread().getUncaughtExceptionHandler());
Thread.currentThread().setUncaughtExceptionHandler(handler);
return handler;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2015 the original author or authors.
* Copyright 2012-2016 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.
@ -82,6 +82,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
@ -552,13 +553,37 @@ public class SpringApplicationTests {
this.context = application.run();
assertNotNull(this.context);
assertEquals(2, SpringApplication.exit(this.context, new ExitCodeGenerator() {
@Override
public int getExitCode() {
return 2;
}
}));
}
@Test
public void exitWithExplicitCodeFromException() throws Exception {
final SpringBootExceptionHandler handler = mock(SpringBootExceptionHandler.class);
SpringApplication application = new SpringApplication(
ExitCodeCommandLineRunConfig.class) {
@Override
SpringBootExceptionHandler getSpringBootExceptionHandler() {
return handler;
}
};
application.setWebEnvironment(false);
try {
application.run();
fail("Did not throw");
}
catch (IllegalStateException ex) {
}
verify(handler).registerExitCode(11);
}
@Test
public void defaultCommandLineArgs() throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class);
@ -858,6 +883,34 @@ public class SpringApplicationTests {
public TestCommandLineRunner runnerA() {
return new TestCommandLineRunner(Ordered.HIGHEST_PRECEDENCE);
}
}
@Configuration
static class ExitCodeCommandLineRunConfig {
@Bean
public CommandLineRunner runner() {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
throw new IllegalStateException(new ExitStatusException());
}
};
}
}
static class ExitStatusException extends RuntimeException
implements ExitCodeGenerator {
@Override
public int getExitCode() {
return 11;
}
}
static class AbstractTestRunner implements ApplicationContextAware, Ordered {