Skip to content

Commit bf2692c

Browse files
committed
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
1 parent 206da74 commit bf2692c

File tree

16 files changed

+1012
-93
lines changed

16 files changed

+1012
-93
lines changed

spring-shell-core/src/main/java/org/springframework/shell/Shell.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,12 @@ private Object evaluate(Input input) {
252252
return ute.getCause();
253253
}
254254
catch (CommandExecutionException e1) {
255-
return e1.getCause();
255+
if (e1.getCause() instanceof Exception e11) {
256+
e = e11;
257+
}
258+
else {
259+
return e1.getCause();
260+
}
256261
}
257262
catch (Exception e2) {
258263
e = e2;
@@ -265,9 +270,13 @@ private Object evaluate(Input input) {
265270
CommandHandlingResult processException = processException(commandExceptionResolvers, e);
266271
processExceptionNonInt = processException;
267272
if (processException != null) {
268-
handlingResultNonInt = e;
269-
this.terminal.writer().append(processException.message());
270-
this.terminal.writer().flush();
273+
if (processException.isPresent()) {
274+
handlingResultNonInt = e;
275+
if (StringUtils.hasText(processException.message())) {
276+
this.terminal.writer().append(processException.message());
277+
this.terminal.writer().flush();
278+
}
279+
}
271280
return null;
272281
}
273282
} catch (Exception e1) {
@@ -298,12 +307,7 @@ private CommandHandlingResult processException(List<CommandExceptionResolver> co
298307
}
299308
}
300309
if (r != null) {
301-
if (r.isEmpty()) {
302-
return null;
303-
}
304-
else {
305-
return r;
306-
}
310+
return r;
307311
}
308312
throw e;
309313
}

spring-shell-core/src/main/java/org/springframework/shell/command/CommandHandlingResult.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,60 +25,60 @@
2525
public interface CommandHandlingResult {
2626

2727
/**
28-
* Gets a message for this {@code HandlingResult}.
28+
* Gets a message for this {@code CommandHandlingResult}.
2929
*
3030
* @return a message
3131
*/
3232
@Nullable
3333
String message();
3434

3535
/**
36-
* Gets an exit code for this {@code HandlingResult}. Exit code only has meaning
36+
* Gets an exit code for this {@code CommandHandlingResult}. Exit code only has meaning
3737
* if shell is in non-interactive mode.
3838
*
3939
* @return an exit code
4040
*/
4141
Integer exitCode();
4242

4343
/**
44-
* Indicate whether this {@code HandlingResult} has a result.
44+
* Indicate whether this {@code CommandHandlingResult} has a result.
4545
*
4646
* @return true if result exist
4747
*/
4848
public boolean isPresent();
4949

5050
/**
51-
* Indicate whether this {@code HandlingResult} does not have a result.
51+
* Indicate whether this {@code CommandHandlingResult} does not have a result.
5252
*
5353
* @return true if result doesn't exist
5454
*/
5555
public boolean isEmpty();
5656

5757
/**
58-
* Gets an empty instance of {@code HandlingResult}.
58+
* Gets an empty instance of {@code CommandHandlingResult}.
5959
*
60-
* @return empty instance of {@code HandlingResult}
60+
* @return empty instance of {@code CommandHandlingResult}
6161
*/
6262
public static CommandHandlingResult empty() {
6363
return of(null);
6464
}
6565

6666
/**
67-
* Gets an instance of {@code HandlingResult}.
67+
* Gets an instance of {@code CommandHandlingResult}.
6868
*
6969
* @param message the message
70-
* @return instance of {@code HandlingResult}
70+
* @return instance of {@code CommandHandlingResult}
7171
*/
7272
public static CommandHandlingResult of(@Nullable String message) {
7373
return of(message, null);
7474
}
7575

7676
/**
77-
* Gets an instance of {@code HandlingResult}.
77+
* Gets an instance of {@code CommandHandlingResult}.
7878
*
7979
* @param message the message
8080
* @param exitCode the exit code
81-
* @return instance of {@code HandlingResult}
81+
* @return instance of {@code CommandHandlingResult}
8282
*/
8383
public static CommandHandlingResult of(@Nullable String message, Integer exitCode) {
8484
return new DefaultHandlingResult(message, exitCode);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.command.annotation;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.aot.hint.annotation.Reflective;
25+
26+
/**
27+
* Annotation for handling exceptions in specific command classes and/or its methods.
28+
*
29+
* @author Janne Valkealahti
30+
*/
31+
@Retention(RetentionPolicy.RUNTIME)
32+
@Target(ElementType.METHOD)
33+
@Documented
34+
@Reflective
35+
public @interface ExceptionResolver {
36+
37+
/**
38+
* Exceptions handled by the annotated method. If empty, will default to any
39+
* exceptions listed in the method argument list.
40+
*
41+
* @return Exceptions handled by annotated method
42+
*/
43+
Class<? extends Throwable>[] value() default {};
44+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.command.annotation;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.springframework.core.ExceptionDepthComparator;
26+
import org.springframework.core.MethodIntrospector;
27+
import org.springframework.core.annotation.AnnotatedElementUtils;
28+
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.ConcurrentReferenceHashMap;
31+
import org.springframework.util.ReflectionUtils.MethodFilter;
32+
33+
/**
34+
*
35+
* @author Janne Valkealahti
36+
*/
37+
public class ExceptionResolverMethodResolver {
38+
39+
private static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
40+
AnnotatedElementUtils.hasAnnotation(method, ExceptionResolver.class);
41+
private static final Method NO_MATCHING_EXCEPTION_HANDLER_METHOD;
42+
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
43+
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
44+
45+
static {
46+
try {
47+
NO_MATCHING_EXCEPTION_HANDLER_METHOD =
48+
ExceptionResolverMethodResolver.class.getDeclaredMethod("noMatchingExceptionHandler");
49+
}
50+
catch (NoSuchMethodException ex) {
51+
throw new IllegalStateException("Expected method not found: " + ex);
52+
}
53+
}
54+
55+
/**
56+
* A constructor that finds {@link ExceptionResolver} methods in the given type.
57+
*
58+
* @param handlerType the type to introspect
59+
*/
60+
public ExceptionResolverMethodResolver(Class<?> handlerType) {
61+
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
62+
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
63+
addExceptionMapping(exceptionType, method);
64+
}
65+
}
66+
}
67+
68+
/**
69+
* Extract exception mappings from the {@code @ExceptionResolver} annotation first,
70+
* and then as a fallback from the method signature itself.
71+
*/
72+
@SuppressWarnings("unchecked")
73+
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
74+
List<Class<? extends Throwable>> result = new ArrayList<>();
75+
detectAnnotationExceptionMappings(method, result);
76+
if (result.isEmpty()) {
77+
for (Class<?> paramType : method.getParameterTypes()) {
78+
if (Throwable.class.isAssignableFrom(paramType)) {
79+
result.add((Class<? extends Throwable>) paramType);
80+
}
81+
}
82+
}
83+
if (result.isEmpty()) {
84+
throw new IllegalStateException("No exception types mapped to " + method);
85+
}
86+
return result;
87+
}
88+
89+
private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
90+
ExceptionResolver ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionResolver.class);
91+
Assert.state(ann != null, "No ExceptionResolver annotation");
92+
result.addAll(Arrays.asList(ann.value()));
93+
}
94+
95+
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
96+
Method oldMethod = this.mappedMethods.put(exceptionType, method);
97+
if (oldMethod != null && !oldMethod.equals(method)) {
98+
throw new IllegalStateException("Ambiguous @ExceptionResolver method mapped for [" +
99+
exceptionType + "]: {" + oldMethod + ", " + method + "}");
100+
}
101+
}
102+
103+
/**
104+
* Whether the contained type has any exception mappings.
105+
*/
106+
public boolean hasExceptionMappings() {
107+
return !this.mappedMethods.isEmpty();
108+
}
109+
110+
/**
111+
* Find a {@link Method} to handle the given exception.
112+
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
113+
* @param exception the exception
114+
* @return a Method to handle the exception, or {@code null} if none found
115+
*/
116+
@Nullable
117+
public Method resolveMethod(Exception exception) {
118+
return resolveMethodByThrowable(exception);
119+
}
120+
121+
/**
122+
* Find a {@link Method} to handle the given Throwable.
123+
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
124+
*
125+
* @param exception the exception
126+
* @return a Method to handle the exception, or {@code null} if none found
127+
*/
128+
@Nullable
129+
public Method resolveMethodByThrowable(Throwable exception) {
130+
Method method = resolveMethodByExceptionType(exception.getClass());
131+
if (method == null) {
132+
Throwable cause = exception.getCause();
133+
if (cause != null) {
134+
method = resolveMethodByThrowable(cause);
135+
}
136+
}
137+
return method;
138+
}
139+
140+
/**
141+
* Find a {@link Method} to handle the given exception type. This can be
142+
* useful if an {@link Exception} instance is not available (e.g. for tools).
143+
* <p>Uses {@link ExceptionDepthComparator} if more than one match is found.
144+
*
145+
* @param exceptionType the exception type
146+
* @return a Method to handle the exception, or {@code null} if none found
147+
*/
148+
@Nullable
149+
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
150+
Method method = this.exceptionLookupCache.get(exceptionType);
151+
if (method == null) {
152+
method = getMappedMethod(exceptionType);
153+
this.exceptionLookupCache.put(exceptionType, method);
154+
}
155+
return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);
156+
}
157+
158+
/**
159+
* Return the {@link Method} mapped to the given exception type, or
160+
* {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none.
161+
*/
162+
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
163+
List<Class<? extends Throwable>> matches = new ArrayList<>();
164+
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
165+
if (mappedException.isAssignableFrom(exceptionType)) {
166+
matches.add(mappedException);
167+
}
168+
}
169+
if (!matches.isEmpty()) {
170+
if (matches.size() > 1) {
171+
matches.sort(new ExceptionDepthComparator(exceptionType));
172+
}
173+
return this.mappedMethods.get(matches.get(0));
174+
}
175+
else {
176+
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
177+
}
178+
}
179+
180+
/**
181+
* For the {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} constant.
182+
*/
183+
@SuppressWarnings("unused")
184+
private void noMatchingExceptionHandler() {
185+
}
186+
}

0 commit comments

Comments
 (0)