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:
@@ -252,7 +252,12 @@ public class Shell {
|
||||
return ute.getCause();
|
||||
}
|
||||
catch (CommandExecutionException e1) {
|
||||
return e1.getCause();
|
||||
if (e1.getCause() instanceof Exception e11) {
|
||||
e = e11;
|
||||
}
|
||||
else {
|
||||
return e1.getCause();
|
||||
}
|
||||
}
|
||||
catch (Exception e2) {
|
||||
e = e2;
|
||||
@@ -265,9 +270,13 @@ public class Shell {
|
||||
CommandHandlingResult processException = processException(commandExceptionResolvers, e);
|
||||
processExceptionNonInt = processException;
|
||||
if (processException != null) {
|
||||
handlingResultNonInt = e;
|
||||
this.terminal.writer().append(processException.message());
|
||||
this.terminal.writer().flush();
|
||||
if (processException.isPresent()) {
|
||||
handlingResultNonInt = e;
|
||||
if (StringUtils.hasText(processException.message())) {
|
||||
this.terminal.writer().append(processException.message());
|
||||
this.terminal.writer().flush();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e1) {
|
||||
@@ -298,12 +307,7 @@ public class Shell {
|
||||
}
|
||||
}
|
||||
if (r != null) {
|
||||
if (r.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.springframework.lang.Nullable;
|
||||
public interface CommandHandlingResult {
|
||||
|
||||
/**
|
||||
* Gets a message for this {@code HandlingResult}.
|
||||
* Gets a message for this {@code CommandHandlingResult}.
|
||||
*
|
||||
* @return a message
|
||||
*/
|
||||
@@ -33,7 +33,7 @@ public interface CommandHandlingResult {
|
||||
String message();
|
||||
|
||||
/**
|
||||
* Gets an exit code for this {@code HandlingResult}. Exit code only has meaning
|
||||
* Gets an exit code for this {@code CommandHandlingResult}. Exit code only has meaning
|
||||
* if shell is in non-interactive mode.
|
||||
*
|
||||
* @return an exit code
|
||||
@@ -41,44 +41,44 @@ public interface CommandHandlingResult {
|
||||
Integer exitCode();
|
||||
|
||||
/**
|
||||
* Indicate whether this {@code HandlingResult} has a result.
|
||||
* Indicate whether this {@code CommandHandlingResult} has a result.
|
||||
*
|
||||
* @return true if result exist
|
||||
*/
|
||||
public boolean isPresent();
|
||||
|
||||
/**
|
||||
* Indicate whether this {@code HandlingResult} does not have a result.
|
||||
* Indicate whether this {@code CommandHandlingResult} does not have a result.
|
||||
*
|
||||
* @return true if result doesn't exist
|
||||
*/
|
||||
public boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Gets an empty instance of {@code HandlingResult}.
|
||||
* Gets an empty instance of {@code CommandHandlingResult}.
|
||||
*
|
||||
* @return empty instance of {@code HandlingResult}
|
||||
* @return empty instance of {@code CommandHandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult empty() {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of {@code HandlingResult}.
|
||||
* Gets an instance of {@code CommandHandlingResult}.
|
||||
*
|
||||
* @param message the message
|
||||
* @return instance of {@code HandlingResult}
|
||||
* @return instance of {@code CommandHandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult of(@Nullable String message) {
|
||||
return of(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an instance of {@code HandlingResult}.
|
||||
* Gets an instance of {@code CommandHandlingResult}.
|
||||
*
|
||||
* @param message the message
|
||||
* @param exitCode the exit code
|
||||
* @return instance of {@code HandlingResult}
|
||||
* @return instance of {@code CommandHandlingResult}
|
||||
*/
|
||||
public static CommandHandlingResult of(@Nullable String message, Integer exitCode) {
|
||||
return new DefaultHandlingResult(message, exitCode);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.command.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
|
||||
/**
|
||||
* Annotation for handling exceptions in specific command classes and/or its methods.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
@Reflective
|
||||
public @interface ExceptionResolver {
|
||||
|
||||
/**
|
||||
* Exceptions handled by the annotated method. If empty, will default to any
|
||||
* exceptions listed in the method argument list.
|
||||
*
|
||||
* @return Exceptions handled by annotated method
|
||||
*/
|
||||
Class<? extends Throwable>[] value() default {};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.command.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.ExceptionDepthComparator;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ExceptionResolverMethodResolver {
|
||||
|
||||
private static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
|
||||
AnnotatedElementUtils.hasAnnotation(method, ExceptionResolver.class);
|
||||
private static final Method NO_MATCHING_EXCEPTION_HANDLER_METHOD;
|
||||
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
|
||||
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
|
||||
|
||||
static {
|
||||
try {
|
||||
NO_MATCHING_EXCEPTION_HANDLER_METHOD =
|
||||
ExceptionResolverMethodResolver.class.getDeclaredMethod("noMatchingExceptionHandler");
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new IllegalStateException("Expected method not found: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor that finds {@link ExceptionResolver} methods in the given type.
|
||||
*
|
||||
* @param handlerType the type to introspect
|
||||
*/
|
||||
public ExceptionResolverMethodResolver(Class<?> handlerType) {
|
||||
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
|
||||
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
|
||||
addExceptionMapping(exceptionType, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract exception mappings from the {@code @ExceptionResolver} annotation first,
|
||||
* and then as a fallback from the method signature itself.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
|
||||
List<Class<? extends Throwable>> result = new ArrayList<>();
|
||||
detectAnnotationExceptionMappings(method, result);
|
||||
if (result.isEmpty()) {
|
||||
for (Class<?> paramType : method.getParameterTypes()) {
|
||||
if (Throwable.class.isAssignableFrom(paramType)) {
|
||||
result.add((Class<? extends Throwable>) paramType);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
throw new IllegalStateException("No exception types mapped to " + method);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
|
||||
ExceptionResolver ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionResolver.class);
|
||||
Assert.state(ann != null, "No ExceptionResolver annotation");
|
||||
result.addAll(Arrays.asList(ann.value()));
|
||||
}
|
||||
|
||||
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
|
||||
Method oldMethod = this.mappedMethods.put(exceptionType, method);
|
||||
if (oldMethod != null && !oldMethod.equals(method)) {
|
||||
throw new IllegalStateException("Ambiguous @ExceptionResolver method mapped for [" +
|
||||
exceptionType + "]: {" + oldMethod + ", " + method + "}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the contained type has any exception mappings.
|
||||
*/
|
||||
public boolean hasExceptionMappings() {
|
||||
return !this.mappedMethods.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given exception.
|
||||
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
|
||||
* @param exception the exception
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethod(Exception exception) {
|
||||
return resolveMethodByThrowable(exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given Throwable.
|
||||
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
|
||||
*
|
||||
* @param exception the exception
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethodByThrowable(Throwable exception) {
|
||||
Method method = resolveMethodByExceptionType(exception.getClass());
|
||||
if (method == null) {
|
||||
Throwable cause = exception.getCause();
|
||||
if (cause != null) {
|
||||
method = resolveMethodByThrowable(cause);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given exception type. This can be
|
||||
* useful if an {@link Exception} instance is not available (e.g. for tools).
|
||||
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
|
||||
*
|
||||
* @param exceptionType the exception type
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
|
||||
Method method = this.exceptionLookupCache.get(exceptionType);
|
||||
if (method == null) {
|
||||
method = getMappedMethod(exceptionType);
|
||||
this.exceptionLookupCache.put(exceptionType, method);
|
||||
}
|
||||
return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link Method} mapped to the given exception type, or
|
||||
* {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none.
|
||||
*/
|
||||
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
|
||||
List<Class<? extends Throwable>> matches = new ArrayList<>();
|
||||
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
|
||||
if (mappedException.isAssignableFrom(exceptionType)) {
|
||||
matches.add(mappedException);
|
||||
}
|
||||
}
|
||||
if (!matches.isEmpty()) {
|
||||
if (matches.size() > 1) {
|
||||
matches.sort(new ExceptionDepthComparator(exceptionType));
|
||||
}
|
||||
return this.mappedMethods.get(matches.get(0));
|
||||
}
|
||||
else {
|
||||
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For the {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} constant.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private void noMatchingExceptionHandler() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.command.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Defines exit code. Typically used with {@link ExceptionResolver}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@Documented
|
||||
@Reflective
|
||||
public @interface ExitCode {
|
||||
|
||||
/**
|
||||
* Exit code value.
|
||||
*
|
||||
* @return exit code
|
||||
* @see #code()
|
||||
*/
|
||||
@AliasFor("code")
|
||||
int value() default 0;
|
||||
|
||||
/**
|
||||
* Exit code value.
|
||||
*
|
||||
* @return exit code
|
||||
* @see #value()
|
||||
*/
|
||||
@AliasFor("value")
|
||||
int code() default 0;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.command.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.command.CommandExceptionResolver;
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
import org.springframework.shell.command.invocation.InvocableShellMethod;
|
||||
import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class MethodCommandExceptionResolver implements CommandExceptionResolver {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(MethodCommandExceptionResolver.class);
|
||||
private final Object bean;
|
||||
private final Terminal terminal;
|
||||
|
||||
public MethodCommandExceptionResolver(Object bean) {
|
||||
this(bean, null);
|
||||
}
|
||||
|
||||
public MethodCommandExceptionResolver(Object bean, Terminal terminal) {
|
||||
Assert.notNull(bean, "Target bean must be set");
|
||||
this.bean = bean;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception ex) {
|
||||
try {
|
||||
ExceptionResolverMethodResolver resolver = new ExceptionResolverMethodResolver(bean.getClass());
|
||||
Method exceptionResolverMethod = resolver.resolveMethodByThrowable(ex);
|
||||
if (exceptionResolverMethod == null) {
|
||||
return null;
|
||||
}
|
||||
InvocableShellMethod invocable = new InvocableShellMethod(bean, exceptionResolverMethod);
|
||||
|
||||
ShellMethodArgumentResolverComposite argumentResolvers = new ShellMethodArgumentResolverComposite();
|
||||
argumentResolvers.addResolver(new TerminalResolver());
|
||||
invocable.setMessageMethodArgumentResolvers(argumentResolvers);
|
||||
|
||||
ArrayList<Throwable> exceptions = new ArrayList<>();
|
||||
Throwable exToExpose = ex;
|
||||
while (exToExpose != null) {
|
||||
exceptions.add(exToExpose);
|
||||
Throwable cause = exToExpose.getCause();
|
||||
exToExpose = (cause != exToExpose ? cause : null);
|
||||
}
|
||||
Object[] arguments = new Object[exceptions.size() + 1];
|
||||
exceptions.toArray(arguments);
|
||||
|
||||
MessageBuilder<Object[]> messageBuilder = MessageBuilder.withPayload(arguments);
|
||||
messageBuilder.setHeader("terminal", terminal);
|
||||
|
||||
MethodParameter returnType = invocable.getReturnType();
|
||||
Class<?> parameterType = returnType.getParameterType();
|
||||
boolean isVoid = void.class.isAssignableFrom(parameterType);
|
||||
Object invoke = invocable.invoke(messageBuilder.build(), arguments);
|
||||
|
||||
Integer ecFromAnn = null;
|
||||
ExitCode ecAnn = AnnotationUtils.findAnnotation(exceptionResolverMethod, ExitCode.class);
|
||||
if (ecAnn != null && ecAnn.code() > 0) {
|
||||
ecFromAnn = ecAnn.code();
|
||||
}
|
||||
|
||||
if (isVoid) {
|
||||
if (ecFromAnn != null) {
|
||||
return CommandHandlingResult.of(null, ecFromAnn);
|
||||
}
|
||||
return CommandHandlingResult.empty();
|
||||
}
|
||||
else if (invoke instanceof CommandHandlingResult result) {
|
||||
if (ecFromAnn != null) {
|
||||
return CommandHandlingResult.of(result.message(), ecFromAnn);
|
||||
}
|
||||
return (CommandHandlingResult)invoke;
|
||||
}
|
||||
else if (invoke instanceof String msg) {
|
||||
if (ecFromAnn != null) {
|
||||
return CommandHandlingResult.of(msg, ecFromAnn);
|
||||
}
|
||||
return CommandHandlingResult.of(msg, 1);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
// TODO: should think how to report this to user without logging
|
||||
log.warn("Failure in @ExceptionResolver", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class TerminalResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return Terminal.class.isAssignableFrom(parameter.getParameterType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
|
||||
Terminal terminal = message.getHeaders().get("terminal", Terminal.class);
|
||||
return terminal;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.command.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ExceptionResolverMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void resolvesFromAnnotation() {
|
||||
ExceptionResolverMethodResolver resolver = new ExceptionResolverMethodResolver(InAnnotation.class);
|
||||
assertThat(resolver.hasExceptionMappings()).isTrue();
|
||||
assertThat(resolver.resolveMethod(new RuntimeException()))
|
||||
.isSameAs(ReflectionUtils.findMethod(InAnnotation.class, "errorHandler"));
|
||||
assertThat(resolver.resolveMethod(new IOException()))
|
||||
.isSameAs(ReflectionUtils.findMethod(InAnnotation.class, "errorHandler"));
|
||||
}
|
||||
|
||||
private static class InAnnotation {
|
||||
|
||||
@ExceptionResolver({ RuntimeException.class, IOException.class })
|
||||
void errorHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesFromMethodParameters() {
|
||||
ExceptionResolverMethodResolver resolver = new ExceptionResolverMethodResolver(InMethodParameter.class);
|
||||
assertThat(resolver.hasExceptionMappings()).isTrue();
|
||||
assertThat(resolver.resolveMethod(new RuntimeException())).isSameAs(ReflectionUtils
|
||||
.findMethod(InMethodParameter.class, "errorHandler", RuntimeException.class, IOException.class));
|
||||
assertThat(resolver.resolveMethod(new IOException())).isSameAs(ReflectionUtils
|
||||
.findMethod(InMethodParameter.class, "errorHandler", RuntimeException.class, IOException.class));
|
||||
}
|
||||
|
||||
private static class InMethodParameter {
|
||||
|
||||
@ExceptionResolver
|
||||
void errorHandler(RuntimeException e1, IOException e2) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.command.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.shell.command.CommandHandlingResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MethodCommandExceptionResolverTests {
|
||||
|
||||
@Test
|
||||
void annoHaveMatchingParameter() {
|
||||
AnnoHaveMatchingParameter bean = new AnnoHaveMatchingParameter();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
CustomException1 e = new CustomException1();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(e).isSameAs(bean.called);
|
||||
}
|
||||
|
||||
private static class AnnoHaveMatchingParameter {
|
||||
|
||||
Exception called;
|
||||
|
||||
@ExceptionResolver({ CustomException1.class })
|
||||
CommandHandlingResult errorHandler(Exception e) {
|
||||
called = e;
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void annoHaveNotMatchingParameter() {
|
||||
AnnoHaveNotMatchingParameter bean = new AnnoHaveNotMatchingParameter();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
CustomException1 e = new CustomException1();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(bean.called).isTrue();
|
||||
}
|
||||
|
||||
private static class AnnoHaveNotMatchingParameter {
|
||||
|
||||
boolean called;
|
||||
|
||||
@ExceptionResolver({ CustomException1.class })
|
||||
CommandHandlingResult errorHandler() {
|
||||
called = true;
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvedFromParameter() {
|
||||
ResolvedFromParameter bean = new ResolvedFromParameter();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
CustomException1 e = new CustomException1();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(e).isSameAs(bean.called);
|
||||
}
|
||||
|
||||
private static class ResolvedFromParameter {
|
||||
|
||||
Exception called;
|
||||
|
||||
@ExceptionResolver
|
||||
CommandHandlingResult errorHandler(CustomException1 e) {
|
||||
called = e;
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMappedExceptions() {
|
||||
NoMappedExceptions bean = new NoMappedExceptions();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
assertThat(resolver).isNotNull();
|
||||
}
|
||||
|
||||
private static class NoMappedExceptions {
|
||||
|
||||
@ExceptionResolver
|
||||
CommandHandlingResult errorHandler() {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldErrorWhenResolving() {
|
||||
ShouldErrorWhenResolving bean = new ShouldErrorWhenResolving();
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean);
|
||||
RuntimeException e = new RuntimeException();
|
||||
CommandHandlingResult result = resolver.resolve(e);
|
||||
// Internal error doesn't get through - IOException cannot be resolved
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
private static class ShouldErrorWhenResolving {
|
||||
|
||||
@ExceptionResolver
|
||||
CommandHandlingResult errorHandler(RuntimeException e1, IOException e2) {
|
||||
return RESULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandHandlingResult RESULT = CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
|
||||
private static class CustomException1 extends RuntimeException {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2022 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
|
||||
*
|
||||
* https://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.shell.command.invocation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InvocableShellMethodTests {
|
||||
|
||||
@Test
|
||||
public void resolveArg() throws Exception {
|
||||
Handler bean = new Handler();
|
||||
Method method = ReflectionUtils.findMethod(Handler.class, "handle", Integer.class, String.class);
|
||||
InvocableShellMethod invocable = new InvocableShellMethod(bean, method);
|
||||
Object value = invocable.invoke(null, 99, "value");
|
||||
assertThat(value).isEqualTo("99-value");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class Handler {
|
||||
|
||||
public String handle(Integer intArg, String stringArg) {
|
||||
return intArg + "-" + stringArg;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|===
|
||||
@@ -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
|
||||
@@ -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]
|
||||
----
|
||||
====
|
||||
@@ -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[]
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,21 @@
|
||||
*/
|
||||
package org.springframework.shell.samples.e2e;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.boot.ExitCodeGenerator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.shell.command.CommandRegistration;
|
||||
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;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
|
||||
/**
|
||||
* Commands used for e2e test.
|
||||
@@ -32,8 +39,55 @@ import org.springframework.shell.standard.ShellComponent;
|
||||
@ShellComponent
|
||||
public class ErrorHandlingCommands extends BaseE2ECommands {
|
||||
|
||||
@ShellMethod(key = LEGACY_ANNO + "error-handling", group = GROUP)
|
||||
String testErrorHandling(String arg1) throws IOException {
|
||||
if ("throw1".equals(arg1)) {
|
||||
throw new CustomException1();
|
||||
}
|
||||
if ("throw2".equals(arg1)) {
|
||||
throw new CustomException2(11);
|
||||
}
|
||||
if ("throw3".equals(arg1)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
if ("throw4".equals(arg1)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if ("throw5".equals(arg1)) {
|
||||
throw new CustomException3();
|
||||
}
|
||||
if ("throw6".equals(arg1)) {
|
||||
throw new CustomException4();
|
||||
}
|
||||
return "Hello " + arg1;
|
||||
}
|
||||
|
||||
@ExceptionResolver({ CustomException1.class })
|
||||
CommandHandlingResult errorHandler1(CustomException1 e) {
|
||||
return CommandHandlingResult.of("Hi, handled custom exception\n", 42);
|
||||
}
|
||||
|
||||
@ExceptionResolver
|
||||
CommandHandlingResult errorHandler2(IllegalArgumentException e) {
|
||||
return CommandHandlingResult.of("Hi, handled illegal exception\n", 42);
|
||||
}
|
||||
|
||||
@ExceptionResolver({ CustomException3.class })
|
||||
@ExitCode(3)
|
||||
String errorHandler3(CustomException3 e) {
|
||||
return "Hi, handled custom exception 3\n";
|
||||
}
|
||||
|
||||
@ExceptionResolver({ CustomException4.class })
|
||||
@ExitCode(code = 4)
|
||||
void errorHandler3(CustomException4 e, Terminal terminal) {
|
||||
PrintWriter writer = terminal.writer();
|
||||
writer.println(String.format("Hi, handled custom exception %s", e));
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandRegistration testErrorHandlingRegistration(Supplier<CommandRegistration.Builder> builder) {
|
||||
CommandRegistration testErrorHandlingRegistration(Supplier<CommandRegistration.Builder> builder) {
|
||||
return builder.get()
|
||||
.command(REG, "error-handling")
|
||||
.group(GROUP)
|
||||
@@ -56,6 +110,16 @@ public class ErrorHandlingCommands extends BaseE2ECommands {
|
||||
if ("throw3".equals(arg1)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
if ("throw4".equals(arg1)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
if ("throw5".equals(arg1)) {
|
||||
throw new CustomException3();
|
||||
}
|
||||
if ("throw6".equals(arg1)) {
|
||||
throw new CustomException4();
|
||||
}
|
||||
|
||||
return "Hello " + arg1;
|
||||
})
|
||||
.and()
|
||||
@@ -79,13 +143,27 @@ public class ErrorHandlingCommands extends BaseE2ECommands {
|
||||
}
|
||||
}
|
||||
|
||||
private static class CustomException3 extends RuntimeException {
|
||||
}
|
||||
|
||||
private static class CustomException4 extends RuntimeException {
|
||||
}
|
||||
|
||||
private static class CustomExceptionResolver implements CommandExceptionResolver {
|
||||
|
||||
@Override
|
||||
public CommandHandlingResult resolve(Exception e) {
|
||||
if (e instanceof CustomException1) {
|
||||
return CommandHandlingResult.of("Hi, handled exception\n", 42);
|
||||
return CommandHandlingResult.of("Hi, handled custom exception\n", 42);
|
||||
}
|
||||
if (e instanceof CustomException3) {
|
||||
return CommandHandlingResult.of("Hi, handled custom exception 3\n", 3);
|
||||
}
|
||||
if (e instanceof CustomException4) {
|
||||
return CommandHandlingResult.of("Hi, handled custom exception\n", 42);
|
||||
}
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
return CommandHandlingResult.of("Hi, handled illegal exception\n", 42);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodParameter;
|
||||
@@ -43,6 +45,7 @@ import org.springframework.shell.command.CommandRegistration;
|
||||
import org.springframework.shell.command.CommandRegistration.Builder;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionArity;
|
||||
import org.springframework.shell.command.CommandRegistration.OptionSpec;
|
||||
import org.springframework.shell.command.annotation.MethodCommandExceptionResolver;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
import org.springframework.shell.standard.ShellOption.NoValueProvider;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -211,6 +214,12 @@ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar {
|
||||
}
|
||||
|
||||
builder.withTarget().method(bean, method);
|
||||
|
||||
ObjectProvider<Terminal> terminal = this.applicationContext.getBeanProvider(Terminal.class);
|
||||
// TODO: feels a bit fishy to return null terminal but for now it's mostly to pass tests as it should not fail
|
||||
MethodCommandExceptionResolver resolver = new MethodCommandExceptionResolver(bean, terminal.getIfAvailable(() -> null));
|
||||
builder.withErrorHandling().resolver(resolver);
|
||||
|
||||
CommandRegistration registration = builder.build();
|
||||
registry.register(registration);
|
||||
}, method -> method.getAnnotation(ShellMethod.class) != null);
|
||||
|
||||
Reference in New Issue
Block a user