Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to handle exceptions in Spring Boot

I was reading the Spring docs and found that creating a subclass from ResponseEntityExceptionHandler was a good way on handling exceptions. However, I tried to handle exceptions in a different way, since I need to diff BusinessExceptions from TechnicalExceptions.

Created a bean called BusinessFault which encapsulates the exception details:

BusinessFault.java

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonInclude(value = Include.NON_NULL)
public class BusinessFault {

    @JsonProperty(value = "category")
    private final String CATEGORY = "Business Failure";
    protected String type;
    protected String code;
    protected String reason;
    protected String description;
    protected String instruction;

    public BusinessFault(String type, String code, String reason) {
        this.type = type;
        this.code = code;
        this.reason = reason;
    }

    public BusinessFault(String type, String code, String reason, String description, String instruction) {
        this.type = type;
        this.code = code;
        this.reason = reason;
        this.description = description;
        this.instruction = instruction;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getInstruction() {
        return instruction;
    }

    public void setInstruction(String instruction) {
        this.instruction = instruction;
    }

    public String getCATEGORY() {
        return CATEGORY;
    }
}

Created a BusinessException class, which do the job by creating a BusinessFault beans through the details passed by its constructor:

BusinessException.java

import com.rest.restwebservices.exception.fault.BusinessFault;

public abstract class BusinessException extends RuntimeException {

    private BusinessFault businessFault;

    public BusinessException(String type, String code, String reason) {
        this.businessFault = new BusinessFault(type, code, reason);
    }

    public BusinessException(String type, String code, String reason, String description, String instruction) {
        this.businessFault = new BusinessFault(type, code, reason, description, instruction);
    }

    public BusinessException(BusinessFault businessFault) {
        this.businessFault = businessFault;
    }

    public BusinessFault getBusinessFault() {
        return businessFault;
    }

    public void setBusinessFault(BusinessFault businessFault) {
        this.businessFault = businessFault;
    }
}

Created a specific UserNotFoundException class, which extends from BusinessException class:

UserNotFoundException.java

import com.rest.restwebservices.exception.fault.BusinessFault;
import com.rest.restwebservices.exception.map.ExceptionMap;

public class UserNotFoundException extends BusinessException {

    public UserNotFoundException(BusinessFault businessFault) {
        super(businessFault);
    }

    public UserNotFoundException(String reason) {
        super(ExceptionMap.USERNOTFOUND.getType(), ExceptionMap.USERNOTFOUND.getCode(), reason);
    }

    public UserNotFoundException(String reason, String description, String instruction) {
        super(ExceptionMap.USERNOTFOUND.getType(), ExceptionMap.USERNOTFOUND.getCode(), reason, description,
                instruction);
    }
}

Created a BusinessExceptionHandler, but instead of being a subclass of ResponseEntityExceptionHandler, it's only has a @ControllerAdvice annotation and a method that handles all thrown BusinessExceptions:

BusinessExceptionHandler.java

import javax.servlet.http.HttpServletRequest;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.rest.restwebservices.controller.UserController;
import com.rest.restwebservices.exception.BusinessException;
import com.rest.restwebservices.exception.fault.BusinessFault;

@ControllerAdvice(basePackageClasses = UserController.class)
public class BusinessExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public ResponseEntity<BusinessFault> genericHandler(HttpServletRequest request, BusinessException ex) {
        return new ResponseEntity<BusinessFault>(ex.getBusinessFault(), HttpStatus.OK);
    }
}

The service layer can throw a UserNotFoundException:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public User findById(Long id) {
        User user = userRepository.findOne(id);
        if (user == null)
            throw new UserNotFoundException("The ID " + id + " doesn't behave to any user!");

        return user;
    }
}

It works fine. But I was wondering if this is a bad practice on handling exceptions?

like image 428
Matheus Cirillo Avatar asked May 28 '18 18:05

Matheus Cirillo


People also ask

How does spring boot handle checked exceptions?

The key is catch the checked exceptions in the application and throw RuntimeExceptions. Let these exceptions be thrown out of Controller class and then Spring applies ControllerAdvice to it.

How do you handle exceptions in a Spring web application?

You can add extra ( @ExceptionHandler ) methods to any controller to specifically handle exceptions thrown by request handling ( @RequestMapping ) methods in the same controller. Such methods can: Handle exceptions without the @ResponseStatus annotation (typically predefined exceptions that you didn't write)


2 Answers

I've got a little problem with your exception handling. Principally it is absolutely ok to catch runtime exceptions, handle them and send them forth to the client, which is probably someone using your REST service and getting the error response as a JSON object. If you manage to tell him what he did wrong and what he can do about it, great! Of course, it will add some complexity to it, but it is probably easy and comfortable to work with that API.

But think about the backend developers, too, that work with your code. Especially the public User findById(Long id) method in your UserService is obscure. The reason for this is that you made your BusinessException, in particular, the UserNotFoundException unchecked.

If I joined your (backend) team, and I was to write some business logic using that service, I'd be quite sure what I had to expect from that method: I pass a user ID and get back a User object if it was found or null if not. That's why I would write code like that

User user = userService.findById("42A");
if (user == null) {
  // create a User or return an error or null or whatever
} else {
  // proceed
}

However, I would never know, that the first condition will never be true since you never return null. How should I know that I had to catch an Exception?

Is the compiler telling me to catch it? No, as it is not checked.

Would I look into your source code? Hell, no! Your case is extremely simple. That UserNotFoundException may be raised in another method in another class among hundred lines of code. Sometimes I couldn't look inside it, anyway, as that UserService is just a compiled class in a dependency.

Do I read the JavaDoc? Hahaha. Let's say, 50% of the time I wouldn't, and the other 50% you've forgotten to document it, anyway.

So, the developer has to wait until his code is used (either by a client or in Unit tests) to see that it doesn't work as he intended, forcing him to redesign what he has coded so far. And if your whole API is designed that way, that unchecked exceptions pop out of nowhere, it can be very very annoying, it costs time and money and is so easy to avoid, actually.

like image 178
Jan B. Avatar answered Sep 29 '22 14:09

Jan B.


I use a similar way to handle exceptions. But in my case, different handlers are managed according to the error status (e.g. an user exists, an user cannot be registered due to some unsatisfied condition, etc.).

You also might add your generic BusinessException for some special cases. Hope it helps you feel better.

import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.rest.restwebservices.controller.UserController;
import com.rest.restwebservices.exception.ResourceNotFoundException;
import com.rest.restwebservices.exception.PreconditionFailedException;
import com.rest.restwebservices.exception.ResourceAlreadyExistsException;
import com.rest.restwebservices.exception.fault.BusinessFault;

@ControllerAdvice(basePackageClasses = UserController.class)
public class BusinessExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseBody
    public ResponseEntity<BusinessFault> genericHandler(HttpServletRequest request, ResourceNotFoundException ex) {
        return new ResponseEntity<BusinessFault>(ex.getBusinessFault(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(PreconditionFailedException.class)
    @ResponseBody
    public ResponseEntity<BusinessFault> genericHandler(HttpServletRequest request, PreconditionFailedExceptionex) {
        return new ResponseEntity<BusinessFault>(ex.getBusinessFault(), HttpStatus.PRECONDITION_FAILED);
    }

    @ExceptionHandler(ResourceAlreadyExistsException.class)
    @ResponseBody
    public ResponseEntity<BusinessFault> genericHandler(HttpServletRequest request, ResourceAlreadyExistsException) {
        return new ResponseEntity<BusinessFault>(ex.getBusinessFault(), HttpStatus.CONFLICT);
    }
}
like image 34
Ara Kokeba Avatar answered Sep 29 '22 16:09

Ara Kokeba