Being able to properly handle exceptions thrown by REST API programs and return friendly exception messages is very important because it helps API clients respond correctly to server problems. This helps improve the quality of service of REST apis. The exception information returned by Spring Boot by default is obscure to API clients, and only developers care about stack exception reports. In this tutorial, we’ll take a look at how to handle Spring REST API exception messages.
Spring Boot has recently become one of the hottest topics in the Java development community, with more and more developers choosing Spring Boot to build REST apis. Using Spring Boot can help developers reduce the amount of template code and configuration file writing. The out-of-the-box feature of Spring Boot is favored by developers. In this tutorial, we’ll focus on some exception handling techniques for the Spring Boot REST API through a streamlined Demo project.
If you don’t want to read this article and just want to get the source code quickly, you can go straight to the end of the article and find the Gihub repository link, where you can easily get the full source code for this article.
1. Clearly define the exception information
In a sense, it is impolite and irresponsible not to send an obscure stack report back to the API client when the program sends an error. Now, we will simulate the requirement that an API client can send a request to a server to obtain one or more user information, and also send a request to create a new user information. Here is a general API information:
The name of the API | instructions |
---|---|
GET /users/{userId} | Retrieves user information based on the user ID. If no exception is found, the user is returned with the exception information |
GET /users | According to the incoming ID set, the user information is retrieved. If the user information is not found, the abnormal information is returned |
POST /users | Create a new user |
Spring MVC provides some useful functionality to help us resolve system exception messages and return useful prompts to the API client.
Take POST/Users to create a new user. When we provide normal user data and request this interface, the REST API will return the following message:
{
"id": 2."username": "wukong"."age": 52."height": 170
}
Copy the code
Now change the user’s age to 200 years old and height to 500 cm. The user name is Rulai.
{
"restapierror": {
"status": "BAD_REQUEST"."timestamp": "The 2019-05-19 06:04:47"."message": "Validation error"."subErrors": [{"object": "user"."field": "height"."rejectedValue": 500."message": "User must be no taller than 250 centimeters."
},
{
"object": "user"."field": "age"."rejectedValue": 200."message": "Users cannot be over 120 years old."}}}]Copy the code
As shown above, when the API client provides incorrect data, the REST API returns a well-formed exception message, timestamp formatted from the original integer timestamp to a readable date + time, along with a detailed enumeration of the error report.
2. Package exception information
To be able to provide a readable JSON-formatted exception message to the API client, we need to introduce the Jackson JSR 310 dependency package into the project and format the date and time in Java according to the date and time template we were given, using the @JsonFormat annotation provided by Jackson JSR 310. Now add the following dependency packages to the Maven pom.xml file:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.8</version>
</dependency>
Copy the code
With the dependencies in place, we need to provide a wrapper class for exception information: RestApiError. It is responsible for encapsulating the exception information thrown by the REST API:
public class RestApiError {
private HttpStatus status;
@JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd hh:mm:ss")
private LocalDateTime timestamp;
private String message;
private String debugMessage;
private List<RestApiSubError> subErrors;
private RestApiError(a){
timestamp = LocalDateTime.now();
}
RestApiError(HttpStatus status){
this(a);this.status = status;
}
RestApiError(HttpStatus status,Throwable ex){
this(a);this.status = status;
this.message = "Unexpected error";
this.debugMessage = ex.getLocalizedMessage();
}
RestApiError(HttpStatus status,String message,Throwable ex){
this(a);this.status = status;
this.message = message;
this.debugMessage = ex.getLocalizedMessage(); }}Copy the code
- The status property records the status of the response. Does it follow all the states of HttpStatus, such as 4xx and 5xx?
- The TIMESTAMP attribute is used to record when an error was sent
- The message property is used to log custom exception messages, usually API client-friendly prompts
- The debugMessage property is used to record more detailed error reports
- The subErrors attribute records information about subexceptions, such as field verification information in user entities
The RestApiSubError class is used to record more detailed exception information, typically reporting failed field verification in an entity class:
abstract class RestApiSubError{}
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
class RestApiValidationError extends RestApiSubError{
private String object;
private String field;
private Object rejectedValue;
private String message;
RestApiValidationError(String object,String message){
this.object = object;
this.message = message; }}Copy the code
The RestApiSubError class is an abstract empty class whose concrete extension will be implemented in RestApiValidationError. The RestApiValidationError class logs reports of property validation failures in entity classes, such as the User object in this tutorial.
Now let’s validate the GET/Users /1 API to retrieve user information with user ID 1:
{
"id": 1."username": "ramostear"."age": 28."height": 170
}
Copy the code
The REST API successfully returns user information. Let’s pass in a user ID that does not exist in the system. Let’s see what the REST API returns:
GET /users/100
{
"restapierror": {
"status": "NOT_FOUND"."timestamp": "The 2019-05-19 06:31:17"."message": "User was not found for parameters {id=100}"}}Copy the code
As you can see from the JSON information above, the REST API returns a friendly prompt for retrieving non-existent user information. In the beginning, we tested providing invalid age and height information for the user. In the next test, we provided an empty user name and observed the REST API return information:
{
"restapierror": {
"status": "BAD_REQUEST"."timestamp": "The 2019-05-19 06:37:46"."message": "Validation error"."subErrors": [{"object": "user"."field": "username"."rejectedValue": ""."message": "Can't be empty."}}}]Copy the code
3. Spring Boot processes exceptions
Spring Boot handles REST API exceptions with three annotations:
- RestController: Annotations that handle the REST API’s concrete operation logic
- @ExceptionHandler: The annotation that handles exceptions thrown from the class annotated by @RestController
- @ControllerAdvice: An annotation that enables the @ExceptionHandler annotation to be processed in one place
The @ControlleraDivice annotation is a new annotation in Spring 3.2 that enables a single method annotated by the @ExceptionHandler annotation to be applied to multiple controllers. When an exception is thrown by a controller, ControllerAdvice automatically matches the corresponding ExceptionHandler based on the type of exception that is currently thrown. When no specific Exception is available, the default Exception handler class is called to handle the Exception thrown by the controller (the default Exception handler class).
Let’s take a look at the process of Spring Application handling controller exception information.
In the figure, the blue arrow represents the normal request and response process, and the red arrow represents the request and response process where exceptions occur.
4. Customize the exception information processing class
Spring Framework’s built-in exception processing classes often cannot meet our actual business requirements, so we need to define an exception processing class that meets the specific situation. In the custom exception processing class, we can encapsulate more detailed exception reports.
Custom exception handling classes allow us to stand on the shoulders of giants and quickly encapsulate our own exception handling classes without having to build a “wheel” from scratch. Now, in order to be ripe to implement custom exception class, information processing and let its normal work, we can directly extend Spring ResponseEntityExceptionHandler offers classes to define user exception information processing. ResponseEntityExceptionHandler has provided a lot of available functions, we only need to extend the class or covering the provide method.
Open the ResponseEntityExceptionHandler class, we can see the source code of the following:
public abstract class ResponseEntityExceptionHandler {
// Unsupported HTTP request method exception message handling method
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(...).{... }// Unsupported HTTP media type exception handling method
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(...).{... }// Unaccepted HTTP media type exception method
protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(...).{... }// Request path parameter missing exception method
protected ResponseEntity<Object> handleMissingPathVariable(...).{... }// Servlet request parameter exception handling method missing
protected ResponseEntity<Object> handleMissingServletRequestParameter(...).{... }// Servlet request binding exception
protected ResponseEntity<Object> handleServletRequestBindingException(...).{... }// Conversion is not supported
protected ResponseEntity<Object> handleConversionNotSupported(...).{... }// Type mismatch
protected ResponseEntity<Object> handleTypeMismatch(...).{... }// The message cannot be retrieved
protected ResponseEntity<Object> handleHttpMessageNotReadable(...).{... }//HTTP messages are not writable
protected ResponseEntity<Object> handleHttpMessageNotWritable(...).{... }// The method argument is invalid
protected ResponseEntity<Object> handleMethodArgumentNotValid(...).{... }// The servlet request part is missing
protected ResponseEntity<Object> handleMissingServletRequestPart(...).{... }// The binding is abnormal
protected ResponseEntity<Object> handleBindException(...).{... }// No handler exception found
protected ResponseEntity<Object> handleNoHandlerFoundException(...).{... }// The asynchronous request timed out
@Nullable
protected ResponseEntity<Object> handleAsyncRequestTimeoutException(...).{... }// Internal exception
protected ResponseEntity<Object> handleExceptionInternal(...).{...}
}
Copy the code
We selectively overwrite several common exception handling methods and add our own custom exception handling methods:
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
protected ResponseEntity<Object> handleUserNotFound(UserNotFoundException ex){
RestApiError apiError = new RestApiError(HttpStatus.NOT_FOUND);
apiError.setMessage(ex.getMessage());
return buildResponseEntity(apiError);
}
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter( MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String error = ex.getParameterName() + " parameter is missing";
return buildResponseEntity(new RestApiError(BAD_REQUEST, error, ex));
}
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported( HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getContentType());
builder.append(" media type is not supported. Supported media types are ");
ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(","));
return buildResponseEntity(new RestApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex)); }... }Copy the code
UserNotFoundException is our custom exception information class. If there is no record of this ID in the database when executing the GET/Users /{userIds} or GET/Users request, A UserNotFoundException message is thrown and the response status code is set to NOT_FOUND. UserNotFoundException:
public class UserNotFoundException extends Exception {
public UserNotFoundException(Class clz,String... searchParams){
super(UserNotFoundException.generateMessage(clz.getSimpleName(),toMap(String.class,String.class,searchParams)));
}
private static String generateMessage(String entity, Map<String,String> searchParams){
return StringUtils.capitalize(entity)+
" was not found for parameters "+
searchParams;
}
private static <K,V> Map<K,V> toMap(Class
key,Class
value,Object... entries)
{
if(entries.length % 2= =1) {throw new IllegalArgumentException("Invalid entries");
}
return IntStream.range(0,entries.length/2).map(i->i*2)
.collect(HashMap::new,
(m,i)->m.put(key.cast(entries[i]),value.cast(entries[i+1])),Map::putAll); }}Copy the code
The following figure more visually illustrates the entire process of custom exception handling:
When an exception occurs in the UserService, the exception message is passed up to the UserController, where it is caught by Spring and redirected to the UserNotFoundException method. UserNotFoundException encapsulates the exception report into the RestApiError object and passes it back to the API Client. With this approach, the API client gets a logical response report.
The full source code for this course has been uploaded to Github repository. You can get the source code here: github.com/ramostear/S…
The original address: www.ramostear.com/articles/sp…