Last active
September 28, 2018 16:22
-
-
Save matsev/3104749 to your computer and use it in GitHub Desktop.
Enhanced error feedback from a Spring Controller
This file contains 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
@XmlRootElement | |
public class ErrorMessage { | |
private List<String> errors; | |
public ErrorMessage() { | |
} | |
public ErrorMessage(List<String> errors) { | |
this.errors = errors; | |
} | |
public ErrorMessage(String error) { | |
this(Collections.singletonList(error)); | |
} | |
public ErrorMessage(String ... errors) { | |
this(Arrays.asList(errors)); | |
} | |
public List<String> getErrors() { | |
return errors; | |
} | |
public void setErrors(List<String> errors) { | |
this.errors = errors; | |
} | |
} |
This file contains 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
/** | |
* Factory interface for creating {@link ErrorMessage} based on a specific {@code Exception}. | |
* @param <T> The specific exception type. | |
*/ | |
public interface ErrorMessageFactory<T extends Exception> { | |
/** | |
* Gets the exception class used for this factory. | |
* @return An exception class. | |
*/ | |
Class<T> getExceptionClass(); | |
/** | |
* Creates an {@link ErrorMessage} from an exception. | |
* @param ex The exception to get data from. | |
* @return An error message. | |
*/ | |
ErrorMessage getErrorMessage(T ex); | |
/** | |
* Gets the HTTP status response code that will be written to the response when the message occurs. | |
* @return An HTTP status response. | |
*/ | |
int getResponseCode(); | |
} |
This file contains 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
public class ErrorMessageHandlerExceptionResolver extends AbstractHandlerExceptionResolver { | |
private static final int DEFAULT_ORDER = 0; | |
private Map<Class<? extends Exception>, ErrorMessageFactory> errorMessageFactories; | |
private HttpMessageConverter<?>[] messageConverters; | |
public ErrorMessageHandlerExceptionResolver() { | |
setOrder(DEFAULT_ORDER); | |
} | |
public void setErrorMessageFactories(ErrorMessageFactory[] errorMessageFactories) { | |
this.errorMessageFactories = new HashMap<>(errorMessageFactories.length); | |
for (ErrorMessageFactory<?> errorMessageFactory : errorMessageFactories) { | |
this.errorMessageFactories.put(errorMessageFactory.getExceptionClass(), errorMessageFactory); | |
} | |
} | |
public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) { | |
this.messageConverters = messageConverters; | |
} | |
@SuppressWarnings("unchecked") | |
@Override | |
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { | |
ErrorMessageFactory errorMessageFactory = errorMessageFactories.get(ex.getClass()); | |
if (errorMessageFactory != null) { | |
response.setStatus(errorMessageFactory.getResponseCode()); | |
ErrorMessage errorMessage = errorMessageFactory.getErrorMessage(ex); | |
ServletWebRequest webRequest = new ServletWebRequest(request, response); | |
try { | |
return handleResponseBody(errorMessage, webRequest); | |
} catch (Exception handlerException) { | |
logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); | |
} | |
} | |
return null; | |
} | |
/** | |
* Copied from {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver} | |
*/ | |
@SuppressWarnings("unchecked") | |
private ModelAndView handleResponseBody(Object returnValue, ServletWebRequest webRequest) | |
throws ServletException, IOException { | |
HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest()); | |
List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept(); | |
if (acceptedMediaTypes.isEmpty()) { | |
acceptedMediaTypes = Collections.singletonList(MediaType.ALL); | |
} | |
MediaType.sortByQualityValue(acceptedMediaTypes); | |
HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse()); | |
Class<?> returnValueType = returnValue.getClass(); | |
if (this.messageConverters != null) { | |
for (MediaType acceptedMediaType : acceptedMediaTypes) { | |
for (HttpMessageConverter messageConverter : this.messageConverters) { | |
if (messageConverter.canWrite(returnValueType, acceptedMediaType)) { | |
messageConverter.write(returnValue, acceptedMediaType, outputMessage); | |
return new ModelAndView(); | |
} | |
} | |
} | |
} | |
if (logger.isWarnEnabled()) { | |
logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " + | |
acceptedMediaTypes); | |
} | |
return null; | |
} | |
} |
This file contains 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
@ExceptionHandler | |
@ResponseStatus(HttpStatus.BAD_REQUEST) | |
@ResponseBody | |
ErrorMessage handleException(MethodArgumentNotValidException ex) { | |
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors(); | |
List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors(); | |
List<String> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size()); | |
String error; | |
for (FieldError fieldError : fieldErrors) { | |
error = fieldError.getField() + ", " + fieldError.getDefaultMessage(); | |
errors.add(error); | |
} | |
for (ObjectError objectError : globalErrors) { | |
error = objectError.getObjectName() + ", " + objectError.getDefaultMessage(); | |
errors.add(error); | |
} | |
return new ErrorMessage(errors); | |
} |
This file contains 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
public class HttpMediaTypeNotSupportedExceptionErrorMessageFactory implements ErrorMessageFactory<HttpMediaTypeNotSupportedException> { | |
@Override | |
public int getResponseCode() { | |
return HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; | |
} | |
@Override | |
public Class<HttpMediaTypeNotSupportedException> getExceptionClass() { | |
return HttpMediaTypeNotSupportedException.class; | |
} | |
@Override | |
public ErrorMessage getErrorMessage(HttpMediaTypeNotSupportedException ex) { | |
String unsupported = "Unsupported content type: " + ex.getContentType(); | |
String supported = "Supported content types: " + MediaType.toString(ex.getSupportedMediaTypes()); | |
return new ErrorMessage(unsupported, supported); | |
} | |
} |
This file contains 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
public class HttpMessageNotReadableExceptionErrorMessageFactory implements ErrorMessageFactory<HttpMessageNotReadableException> { | |
@Override | |
public int getResponseCode() { | |
return HttpServletResponse.SC_BAD_REQUEST; | |
} | |
@Override | |
public Class<HttpMessageNotReadableException> getExceptionClass() { | |
return HttpMessageNotReadableException.class; | |
} | |
@Override | |
public ErrorMessage getErrorMessage(HttpMessageNotReadableException ex) { | |
Throwable mostSpecificCause = ex.getMostSpecificCause(); | |
if (mostSpecificCause != null) { | |
String exceptionName = mostSpecificCause.getClass().getName(); | |
String message = mostSpecificCause.getMessage(); | |
return new ErrorMessage(exceptionName, message); | |
} | |
return new ErrorMessage(ex.getMessage()); | |
} | |
} |
This file contains 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
public class MethodArgumentNotValidExceptionErrorMessageFactory implements ErrorMessageFactory<MethodArgumentNotValidException> { | |
@Override | |
public Class<MethodArgumentNotValidException> getExceptionClass() { | |
return MethodArgumentNotValidException.class; | |
} | |
@Override | |
public int getResponseCode() { | |
return HttpServletResponse.SC_BAD_REQUEST; | |
} | |
@Override | |
public ErrorMessage getErrorMessage(MethodArgumentNotValidException ex) { | |
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors(); | |
List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors(); | |
List<String> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size()); | |
String error; | |
for (FieldError fieldError : fieldErrors) { | |
error = fieldError.getField() + ", " + fieldError.getDefaultMessage(); | |
errors.add(error); | |
} | |
for (ObjectError objectError : globalErrors) { | |
error = objectError.getObjectName() + ", " + objectError.getDefaultMessage(); | |
errors.add(error); | |
} | |
return new ErrorMessage(errors); | |
} | |
} |
This file contains 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
@XmlRootElement | |
public class User { | |
@NotBlank | |
@Length(min = 3, max = 30) | |
private String name; | |
private String email; | |
// Getters and setters omitted | |
} |
This file contains 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
@RequestMapping(value = "/user/{userId}", | |
method = RequestMethod.PUT, | |
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}) | |
@ResponseStatus(HttpStatus.NO_CONTENT) | |
void update(@PathVariable("userId") long userId, | |
@RequestBody @Valid User user) { | |
userService.update(userId, user); | |
} |
This file contains 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
{ | |
"errors": [ | |
"name, may not be empty", | |
"email, not a well-formed email address" | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The code in this gist shows how the response from a Spring Controller can be enhanced to provide more information to the client. It is explained in more detail in two blog posts, http://www.jayway.com/2012/09/16/improve-your-spring-rest-api-part-i and http://www.jayway.com/2012/09/23/improve-your-spring-rest-api-part-ii .