Support exception handling with annotated methods

- New annotations ExceptionResolver and ExitCode
- New needed functionality is in classes ExceptionResolverMethodResolver
  and MethodCommandExceptionResolver.
- Hook these annotations with StandardMethodTargetRegistrar and Shell classes
- Fixes #597
This commit is contained in:
Janne Valkealahti
2023-01-01 14:51:05 +00:00
parent 206da7463b
commit bf2692c70c
16 changed files with 1012 additions and 93 deletions

View File

@@ -0,0 +1,87 @@
[[dynamic-command-exitcode-annotation]]
==== @ExceptionResolver
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
`@ShellComponent` classes can have `@ExceptionResolver` methods to handle exceptions from component
methods. These are meant for annotated methods.
The exception may match against a top-level exception being propagated (e.g. a direct IOException
being thrown) or against a nested cause within a wrapper exception (e.g. an IOException wrapped
inside an IllegalStateException). This can match at arbitrary cause levels.
For matching exception types, preferably declare the target exception as a method argument, as
the preceding example(s) shows. When multiple exception methods match, a root exception match is
generally preferred to a cause exception match. More specifically, the ExceptionDepthComparator
is used to sort exceptions based on their depth from the thrown exception type.
Alternatively, the annotation declaration may narrow the exception types to match, as the
following example shows:
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=exception-resolver-with-type-in-annotation]
----
====
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=exception-resolver-with-type-in-method]
----
====
`@ExceptionResolver` can also return `String` which is used as an output to console. You can
use `@ExitCode` annotation to define return code.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=exception-resolver-with-exitcode-annotation]
----
====
`@ExceptionResolver` with `void` return type is automatically handled as handled exception.
You can then also define `@ExitCode` and use `Terminal` if you need to write something
into console.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=exception-resolver-with-void]
----
====
===== Method Arguments
`@ExceptionResolver` methods support the following arguments:
[Attributes]
|===
|Method argument |Description
|Exception type
|For access to the raised exception. This is any type of `Exception` or `Throwable`.
|Terminal
|For access to underlying `JLine` terminal to i.e. get its terminal writer.
|===
===== Return Values
`@ExceptionResolver` methods support the following return values:
[Attributes]
|===
|Return value |Description
|String
|Plain text to return to a shell. Exit code 1 is used in this case.
|CommandHandlingResult
|Plain `CommandHandlingResult` having message and exit code.
|void
|A method with a void return type is considered to have fully handled the exception. Usually
you would define `Terminal` as a method argument and write response using _terminal writer_
from it. As exception is fully handled, Exit code 0 is used in this case.
|===

View File

@@ -0,0 +1,34 @@
[[dynamic-command-exitcode-mappings]]
==== Exit Code Mappings
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Default behaviour of an exit codes is as:
- Errors from a command option parsing will result code of `2`
- Any generic error will result result code of `1`
- Obviously in any other case result code is `0`
Every `CommandRegistration` can define its own mappings between _Exception_ and _exit code_.
Essentially we're bound to functionality in `Spring Boot` regarding _exit code_ and simply
integrate into that.
Assuming there is an exception show below which would be thrown from a command:
====
[source, java, indent=0]
----
include::{snippets}/ExitCodeSnippets.java[tag=my-exception-class]
----
====
It is possible to define a mapping function between `Throwable` and exit code. You can also
just configure a _class_ to _exit code_ which is just a syntactic sugar within configurations.
====
[source, java, indent=0]
----
include::{snippets}/ExitCodeSnippets.java[tag=example1]
----
====
NOTE: Exit codes cannot be customized with annotation based configuration

View File

@@ -0,0 +1,47 @@
[[dynamic-command-exitcode-resolving]]
==== Exception Resolving
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Unhandled exceptions will bubble up into shell's `ResultHandlerService` and then eventually
handled by some instance of `ResultHandler`. Chain of `ExceptionResolver` implementations
can be used to resolve exceptions and gives you flexibility to return message to get written
into console together with exit code which are wrapped within `CommandHandlingResult`.
`CommandHandlingResult` may contain a _message_ and/or _exit code_.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=my-exception-resolver-class]
----
====
`CommandExceptionResolver` implementations can be defined globally as bean.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=my-exception-resolver-class-as-bean]
----
====
or defined per `CommandRegistration` if it's applicable only for a particular command itself.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=example1]
----
====
NOTE: Resolvers defined with a command are handled before global resolvers.
Use you own exception types which can also be an instance of boot's `ExitCodeGenerator` if
you want to define exit code there.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=my-exception-class]
----
====

View File

@@ -2,77 +2,18 @@
=== Exception Handling
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Exceptions happen from a user code wether it is intentional or not. This section describes
how `spring-shell` handles exceptions and gives instructions and best practices how to
work with it.
Many command line applications when applicable return an _exit code_ which running environment
can use to differentiate if command has been executed successfully or not. In a `spring-shell`
this mostly relates when a command is run on a non-interactive mode meaning one command
is always executed once with an instance of a `spring-shell`.
is always executed once with an instance of a `spring-shell`. Take a note that _exit code_
always relates to non-interactive shell.
==== Exception Resolving
include::using-shell-commands-exceptionhandling-resolving.adoc[]
Unhandled exceptions will bubble up into shell's `ResultHandlerService` and then eventually
handled by some instance of `ResultHandler`. Chain of `ExceptionResolver` implementations
can be used to resolve exceptions and gives you flexibility to return message to get written
into console together with exit code which are wrapped within `CommandHandlingResult`.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=my-exception-resolver-class]
----
====
`CommandExceptionResolver` implementations can be defined globally as beans or defined
per `CommandRegistration` if it's applicable only for a particular command itself.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=example1]
----
====
Use you own exception types which can also be an instance of boot's `ExitCodeGenerator` if
you want to define exit code there.
====
[source, java, indent=0]
----
include::{snippets}/ErrorHandlingSnippets.java[tag=my-exception-class]
----
====
NOTE: With annotation based configuration exception resolving can only be customised globally
==== Exit Code Mappings
Default behaviour of an exit codes is as:
- Errors from a command option parsing will result code of `2`
- Any generic error will result result code of `1`
- Obviously in any other case result code is `0`
Every `CommandRegistration` can define its own mappings between _Exception_ and _exit code_.
Essentially we're bound to functionality in `Spring Boot` regarding _exit code_ and simply
integrate into that.
Assuming there is an exception show below which would be thrown from a command:
====
[source, java, indent=0]
----
include::{snippets}/ExitCodeSnippets.java[tag=my-exception-class]
----
====
It is possible to define a mapping function between `Throwable` and exit code. You can also
just configure a _class_ to _exit code_ which is just a syntactic sugar within configurations.
====
[source, java, indent=0]
----
include::{snippets}/ExitCodeSnippets.java[tag=example1]
----
====
NOTE: Exit codes cannot be customized with annotation based configuration
include::using-shell-commands-exceptionhandling-mappings.adoc[]
include::using-shell-commands-exceptionhandling-annotation.adoc[]

View File

@@ -15,14 +15,28 @@
*/
package org.springframework.shell.docs;
import org.springframework.shell.command.CommandRegistration;
import java.io.PrintWriter;
import org.jline.terminal.Terminal;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandExceptionResolver;
import org.springframework.shell.command.CommandHandlingResult;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.annotation.ExceptionResolver;
import org.springframework.shell.command.annotation.ExitCode;
class ErrorHandlingSnippets {
// tag::my-exception-class[]
static class CustomException extends RuntimeException {}
static class CustomException extends RuntimeException implements ExitCodeGenerator {
@Override
public int getExitCode() {
return 0;
}
}
// end::my-exception-class[]
// tag::my-exception-resolver-class[]
@@ -48,4 +62,60 @@ class ErrorHandlingSnippets {
// end::example1[]
}
static class Dump1 {
// tag::exception-resolver-with-type-in-annotation[]
@ExceptionResolver({ RuntimeException.class })
CommandHandlingResult errorHandler(Exception e) {
// Exception would be type of RuntimeException,
// optionally do something with it
return CommandHandlingResult.of("Hi, handled exception\n", 42);
}
// end::exception-resolver-with-type-in-annotation[]
}
static class Dump2 {
// tag::exception-resolver-with-type-in-method[]
@ExceptionResolver
CommandHandlingResult errorHandler(RuntimeException e) {
return CommandHandlingResult.of("Hi, handled custom exception\n", 42);
}
// end::exception-resolver-with-type-in-method[]
}
static class Dump3 {
// tag::my-exception-resolver-class-as-bean[]
@Bean
CustomExceptionResolver customExceptionResolver() {
return new CustomExceptionResolver();
}
// end::my-exception-resolver-class-as-bean[]
}
static class Dump4 {
// tag::exception-resolver-with-exitcode-annotation[]
@ExceptionResolver
@ExitCode(code = 5)
String errorHandler(Exception e) {
return "Hi, handled exception";
}
// end::exception-resolver-with-exitcode-annotation[]
}
static class Dump5 {
// tag::exception-resolver-with-void[]
@ExceptionResolver
@ExitCode(code = 5)
void errorHandler(Exception e, Terminal terminal) {
PrintWriter writer = terminal.writer();
String msg = "Hi, handled exception " + e.toString();
writer.println(msg);
writer.flush();
}
// end::exception-resolver-with-void[]
}
}