If you consider this behaviour problematic, and you need fine control over your System.exit
calls, then the only thing you can do is wrap the System.exit functionality in your own logic. If we do that, we can get finally blocks executed and get resources closed as part of our exit flow.
What I'm considering doing is wrapping the System.exit
call & functionality in my own static method. In my implementation of exit
I would throw a custom subclass of Throwable
or Error
, and implement a custom Uncaught exception handler with Thread.setDefaultUncaughtExceptionHandler
to handle that exception. Thus my code becomes:
//in initialization logic:
Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
if(exception instanceof SystemExitEvent){
System.exit(((SystemExitEvent)exception).exitCode);
}
})
// in "main flow" or "close button" or whatever
public void mainFlow(){
try {
businessLogic();
Utilities.exit(0);
}
finally {
cleanUpFileSystemOrDatabaseConnectionOrWhatever();
}
}
//...
class Utilities {
// I'm not a fan of documentaiton,
// but this method could use it.
public void exit(int exitCode){
throw new SystemExitEvent(exitCode);
}
}
class SystemExitEvent extends Throwable {
private final int exitCode;
public SystemExitEvent(int exitCode){
super("system is shutting down")
this.exitCode = exitCode;
}
}
This strategy has the added "benefit" of making this logic testable: to test that the method containing our "main flow" actually requests the system to exit, all we have to do is catch a throwable and assert that is the write type. For example, a test for our business logic wrapper might look like:
//kotlin, a really nice language particularly for testing on the JVM!
@Test fun `when calling business logic should business the business`(){
//setup
val underTest = makeComponentUnderTest(configureToReturnExitCode = 42);
//act
val thrown: SystemExitEvent = try {
underTest.mainFlow();
fail("System Exit event not thrown!")
}
catch(event: SystemExitEvent){
event;
}
//assert
assertThat(thrown.exitCode).isEqualTo(42)
The major downside to this strategy is that it is a way of getting functionality out of exception flow, which often has unintended consequences. The most obvious one, in this case, is that anywhere you've written try { ... } catch(Throwable ex){ /*doesnt rethrow*/ }
will have to be updated. In the case of libraries that have custom execution contexts, they will need to be retrofitted to also understand this exception.
On balance, this seems like a good strategy to me. Does anybody else here think so?