Created
November 5, 2020 19:45
-
-
Save caprica/a3470ced9f16d264fddf786e88be2405 to your computer and use it in GitHub Desktop.
A way to globally map application exceptions to particular response codes in a Spring Boot WebFlux application.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import lombok.extern.slf4j.Slf4j; | |
import org.springframework.boot.web.error.ErrorAttributeOptions; | |
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; | |
import org.springframework.stereotype.Component; | |
import org.springframework.web.reactive.function.server.ServerRequest; | |
import java.lang.reflect.Method; | |
import java.util.Arrays; | |
import java.util.Map; | |
/** | |
* A global exception handler for use with Spring WebFlux controllers. | |
* <p> | |
* Rather than repeating exception streams in the controller code, perform some automatic mapping of unhandled | |
* exceptions to a particular response code and optional extra detail message. | |
* <p> | |
* Why not just use the default exception message? | |
* <p> | |
* Sometimes you do not want a technical message exposed and instead the response code is enough. | |
* <p> | |
* It is also conceivable that an error returned via the API, perhaps for display to a user, has a different format than | |
* the technical error message of the exception. | |
* <p> | |
* In some environments, the default "error" property is stripped from the response so an alternate "detail" property | |
* is used. | |
* | |
* @see UseStatus | |
* @see UseMessage | |
*/ | |
@Component | |
@Slf4j | |
public class GlobalExceptionHandler extends DefaultErrorAttributes { | |
@Override | |
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { | |
Map<String, Object> errorAttributes = super.getErrorAttributes(request, options); | |
Throwable error = getError(request); | |
UseStatus responseStatus = getResponseStatus(error); | |
if (responseStatus != null) { | |
errorAttributes.replace("error", responseStatus.value().getReasonPhrase()); | |
errorAttributes.replace("status", responseStatus.value().value()); | |
String detail = getDetailMessage(error); | |
if (detail != null) { | |
errorAttributes.put("detail", detail); | |
} | |
errorAttributes.remove("trace"); | |
} | |
return errorAttributes; | |
} | |
/** | |
* Get the response status annotation, if there is one, from the class of the thrown exception. | |
* | |
* @param error thrown exception | |
* @return response status annotation, may be <code>null</code> | |
*/ | |
private UseStatus getResponseStatus(Throwable error) { | |
return error.getClass().getAnnotation(UseStatus.class); | |
} | |
/** | |
* Get the optional detail message for a thrown exception. | |
* | |
* @param error thrown exception | |
* @return detail message, may be <code>null</code> | |
*/ | |
private String getDetailMessage(Throwable error) { | |
UseMessage useMessage = error.getClass().getAnnotation(UseMessage.class); | |
if (useMessage != null) { | |
return error.getMessage(); | |
} else { | |
return Arrays.stream(error.getClass().getMethods()) | |
.filter(method -> method.isAnnotationPresent(UseMessage.class)) | |
.filter(method -> method.getParameterCount() == 0) | |
.filter(method -> method.getReturnType().equals(String.class)) | |
.findAny() | |
.map(method -> invoke(method, error)) | |
.orElse(null); | |
} | |
} | |
/** | |
* Invoke the exception detail method. | |
* | |
* @param method method to invoke | |
* @param error object on which to invoke the method | |
* @return exception detail message, may be <code>null</code> | |
*/ | |
private String invoke(Method method, Throwable error) { | |
try { | |
return (String) method.invoke(error); | |
} catch (Exception e) { | |
log.error("Failed to get detail message", e); | |
return e.getMessage(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* Annotation to optionally mark a method in an exception that should be used to obtain the detail message when handled | |
* by the {@link GlobalExceptionHandler}. | |
* <p> | |
* Annotate the class itself to use the default detail message. | |
* <p> | |
* Annotate any single method, that takes no arguments and returns a String, to use that method to obtain the detail | |
* message. | |
* <p> | |
* Not using an annotation will result in no detail message being used. | |
*/ | |
@Target({ ElementType.TYPE, ElementType.METHOD }) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface UseMessage { | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.springframework.http.HttpStatus; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* Annotation to mark an exception that should be handled by the {@link GlobalExceptionHandler}. | |
* <p> | |
* The unhandled system error will be mapped instead to a specific HTTP response code, with the message from this | |
* exception. | |
*/ | |
@Target(ElementType.TYPE) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface UseStatus { | |
HttpStatus value(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment