Thursday, December 31, 2020

Spring boot exception handling – @ExceptionHandler example

 In this spring boot exception handler tutorial, we will learn to validate request body sent to PUT/POST REST APIs. We will also learn to add custom error messages in API responses for validation errors.

Table of Contents

1. Create REST APIs and model classes
2. Request validation and exception handling
3. Demo of request validations
4. Available annotations to use
5. Summary

In this spring boot example, we will see primarily two major validation cases –

  1. HTTP POST /employees and request body does not contain valid values or some fields are missing. It will return HTTP status code 400 with proper message in response body.
  2. HTTP GET /employees/{id} and INVALID ID is sent in request. It will return HTTP status code 404 with proper message in response body.

1. Create REST APIs and model classes

Given REST APIs are from employee management module.

EmployeeRESTController.java
@PostMapping(value = "/employees")
public ResponseEntity<EmployeeVO> addEmployee (@RequestBody EmployeeVO employee)
{
    EmployeeDB.addEmployee(employee);
    return new ResponseEntity<EmployeeVO>(employee, HttpStatus.OK);
}
 
@GetMapping(value = "/employees/{id}")
public ResponseEntity<EmployeeVO> getEmployeeById (@PathVariable("id") int id)
{
    EmployeeVO employee = EmployeeDB.getEmployeeById(id);
     
    if(employee == null) {
         throw new RecordNotFoundException("Invalid employee id : " + id);
    }
    return new ResponseEntity<EmployeeVO>(employee, HttpStatus.OK);
}
EmployeeVO.java
@XmlRootElement(name = "employee")
@XmlAccessorType(XmlAccessType.FIELD)
public class EmployeeVO extends ResourceSupport implements Serializable
{
    private Integer employeeId;
    private String firstName;
    private String lastName;
    private String email;
 
    public EmployeeVO(Integer id, String firstName, String lastName, String email) {
        super();
        this.employeeId = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }
 
    public EmployeeVO() {
    }
     
    //Removed setter/getter for readability
}

2. Spring boot exception handling – REST request validation

2.1. Default spring validation support

To apply default validation, we only need to add relevant annotations in proper places. i.e.

  1. Annotate model class with required validation specific annotations such as @NotEmpty@Email etc.
    @XmlRootElement(name = "employee")
    @XmlAccessorType(XmlAccessType.FIELD)
    public class EmployeeVO extends ResourceSupport implements Serializable
    {
        private static final long serialVersionUID = 1L;
         
        public EmployeeVO(Integer id, String firstName, String lastName, String email) {
            super();
            this.employeeId = id;
            this.firstName = firstName;
            this.lastName = lastName;
            this.email = email;
        }
     
        public EmployeeVO() {
        }
     
        private Integer employeeId;
     
        @NotEmpty(message = "first name must not be empty")
        private String firstName;
     
        @NotEmpty(message = "last name must not be empty")
        private String lastName;
     
        @NotEmpty(message = "email must not be empty")
        @Email(message = "email should be a valid email")
        private String email;
         
        //Removed setter/getter for readability
    }
  2. Enable validation of request body by @Valid annotation
    @PostMapping(value = "/employees")
    public ResponseEntity<EmployeeVO> addEmployee (@Valid @RequestBody EmployeeVO employee)
    {
        EmployeeDB.addEmployee(employee);
        return new ResponseEntity<EmployeeVO>(employee, HttpStatus.OK);
    }

2.2. Exception model classes

Default spring validation works and provide information overload about error, and that’s why we should customize it according to our application’s need. We shall provide only required error information with very clear wordings. Extra information is also not suggested.

It is always a good advise to create exceptions that are meaningful and describe the problem well enough. One way is to create seperate classes to denote specific business usecase failure and return them when that usecase fail.

e.g. I have created RecordNotFoundException class for all buch scenarios where a resource is requested by it’s ID, and resource is not found in the system.

RecordNotFoundException.java
package com.howtodoinjava.demo.exception;
 
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
 
@ResponseStatus(HttpStatus.NOT_FOUND)
public class RecordNotFoundException extends RuntimeException
{
    public RecordNotFoundException(String exception) {
        super(exception);
    }
}

Similarly, I have wrote an special class which will be returned for all failure cases. Having consistent error message structure for all APIs, help the API consumers to write more robust code.

ErrorResponse.java
import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;
 
@XmlRootElement(name = "error")
public class ErrorResponse
{
    public ErrorResponse(String message, List<String> details) {
        super();
        this.message = message;
        this.details = details;
    }
 
    //General error message about nature of error
    private String message;
 
    //Specific errors in API request processing
    private List<String> details;
 
    //Getter and setters
}

2.3. Custom ExceptionHandler

Now add one class extending ResponseEntityExceptionHandler and annotate it with @ControllerAdvice annotation.

ResponseEntityExceptionHandler is a convenient base class for to provide centralized exception handling across all @RequestMapping methods through @ExceptionHandler methods. @ControllerAdvice is more for enabling auto-scanning and configuration at application startup.

Java program for @ControllerAdvice exception handling example.

CustomExceptionHandler.java
package com.howtodoinjava.demo.exception;
 
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
 
@SuppressWarnings({"unchecked","rawtypes"})
@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler
{
    @ExceptionHandler(Exception.class)
    public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        List<String> details = new ArrayList<>();
        details.add(ex.getLocalizedMessage());
        ErrorResponse error = new ErrorResponse("Server Error", details);
        return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
 
    @ExceptionHandler(RecordNotFoundException.class)
    public final ResponseEntity<Object> handleUserNotFoundException(RecordNotFoundException ex, WebRequest request) {
        List<String> details = new ArrayList<>();
        details.add(ex.getLocalizedMessage());
        ErrorResponse error = new ErrorResponse("Record Not Found", details);
        return new ResponseEntity(error, HttpStatus.NOT_FOUND);
    }
 
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        List<String> details = new ArrayList<>();
        for(ObjectError error : ex.getBindingResult().getAllErrors()) {
            details.add(error.getDefaultMessage());
        }
        ErrorResponse error = new ErrorResponse("Validation Failed", details);
        return new ResponseEntity(error, HttpStatus.BAD_REQUEST);
    }
}

Above class handles multiple exceptions including RecordNotFoundException; and it also handle request validation errors in @RequestBody annotated object. Let’s see how it works.

3. Spring boot exception handling – Demo

1) HTTP GET /employees/1 [VALID]

HTTP Status : 200
 
{
    "employeeId": 1,
    "firstName": "John",
    "lastName": "Wick",
    "email": "howtodoinjava@gmail.com",
}

2) HTTP GET /employees/23 [INVALID]

HTTP Status : 404
 
{
    "message": "Record Not Found",
    "details": [
        "Invalid employee id : 23"
    ]
}

3) HTTP POST /employees [INVALID]

Request
{
    "lastName": "Bill",
    "email": "ibill@gmail.com"
}
Response
HTTP Status : 400
 
{
    "message": "Validation Failed",
    "details": [
        "first name must not be empty"
    ]
}

4) HTTP POST /employees [INVALID]

Request
{
    "email": "ibill@gmail.com"
}
Response
HTTP Status : 400
 
{
    "message": "Validation Failed",
    "details": [
        "last name must not be empty",
        "first name must not be empty"
    ]
}

5) HTTP POST /employees [INVALID]

Request
{
    "firstName":"Lokesh",
    "email": "ibill_gmail.com" //invalid email in request
}
Response
HTTP Status : 400
 
{
    "message": "Validation Failed",
    "details": [
        "last name must not be empty",
        "email should be a valid email"
    ]
}

4. REST request validation annotations

In above example, we used only few annotations such as @NotEmpty and @Email. There are more such annotations to validate request data. Check them out when needed.

ANNOTATIONUSAGE
@AssertFalseThe annotated element must be false.
@AssertTrueThe annotated element must be true.
@DecimalMaxThe annotated element must be a number whose value must be lower or equal to the specified maximum.
@DecimalMinThe annotated element must be a number whose value must be higher or equal to the specified minimum.
@FutureThe annotated element must be an instant, date or time in the future.
@MaxThe annotated element must be a number whose value must be lower or equal to the specified maximum.
@MinThe annotated element must be a number whose value must be higher or equal to the specified minimum.
@NegativeThe annotated element must be a strictly negative number.
@NotBlankThe annotated element must not be null and must contain at least one non-whitespace character.
@NotEmptyThe annotated element must not be null nor empty.
@NotNullThe annotated element must not be null.
@NullThe annotated element must be null.
@PatternThe annotated CharSequence must match the specified regular expression.
@PositiveThe annotated element must be a strictly positive number.
@SizeThe annotated element size must be between the specified boundaries (included).

5. Summary

In this spring REST validation tutorial, we learned to –

  • validate ID when fetching resource by ID.
  • validate request body fields in POST/PUT APIs.
  • send consistent and structured error response in API responses.

Drop me your questions related to spring rest exception handling.

No comments:

Post a Comment

How to DROP SEQUENCE in Oracle?

  Oracle  DROP SEQUENCE   overview The  DROP SEQUENCE  the statement allows you to remove a sequence from the database. Here is the basic sy...