Skip to content

Instantly share code, notes, and snippets.

@caprica
Created November 5, 2020 19:45
Show Gist options
  • Save caprica/a3470ced9f16d264fddf786e88be2405 to your computer and use it in GitHub Desktop.
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.
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();
}
}
}
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 {
}
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