- Use @ ControllerAdvice and @ ExceptionHandler to handle global exceptions
- @ExceptionHandler handles Controller level exceptions
- ResponseStatusException
Through this article, you can understand how to handle exceptions in Spring Boot. However, just being able to use it is not enough. We have to think about how to write the code of exception handling with a little elegance. Next, I will talk about a slightly elegant exception handling solution in the way of exception handling in a practical project that I learned from my work.
Final effect display
The returned information includes the following five parts of the exception:
- code that uniquely identifies the exception
- HTTP status code
- Wrong path
- Time stamp where the error occurred
- Error details
In this way, the abnormal information is returned, which is more conducive to our front end to make corresponding performance according to the abnormal information.
Exception handling core code
ErrorCode.java (this enumeration class contains the unique ID of the exception, HTTP status code and error information)
The main function of this class is to unify the possible exceptions in the management system, which is relatively clear. However, the possible problem is that when the system is too complex and there are too many exceptions, the class will be relatively large. There is a solution: unify a variety of similar exceptions into one. For example, unify the exceptions that the user can't find and the exceptions that the order information can't find into the exception of "the resource can't be found". Then the front end will handle the corresponding situation in detail (my personal treatment method, I can't guarantee it is a better way).
import org.springframework.http.HttpStatus; publicenum ErrorCode { RESOURCE_NOT_FOUND(1001, HttpStatus.NOT_FOUND, "The resource was not found"), REQUEST_VALIDATION_FAILED(1002, HttpStatus.BAD_REQUEST, "Request data format validation failed"); privatefinalint code; privatefinal HttpStatus status; privatefinal String message; ErrorCode(int code, HttpStatus status, String message) { this.code = code; this.status = status; this.message = message; } public int getCode() { return code; } public HttpStatus getStatus() { return status; } public String getMessage() { return message; } @Override public String toString() { return"ErrorCode{" + "code=" + code + ", status=" + status + ", message='" + message + '\'' + '}'; } }
ErrorReponse.java (returned to the client specific exception object)
This class is returned to the client as exception information, which includes all the information we want to return to the client when an exception occurs.
import org.springframework.util.ObjectUtils; import java.time.Instant; import java.util.HashMap; import java.util.Map; publicclass ErrorReponse { privateint code; privateint status; private String message; private String path; private Instant timestamp; private HashMap<String, Object> data = new HashMap<String, Object>(); public ErrorReponse() { } public ErrorReponse(BaseException ex, String path) { this(ex.getError().getCode(), ex.getError().getStatus().value(), ex.getError().getMessage(), path, ex.getData()); } public ErrorReponse(int code, int status, String message, String path, Map<String, Object> data) { this.code = code; this.status = status; this.message = message; this.path = path; this.timestamp = Instant.now(); if (!ObjectUtils.isEmpty(data)) { this.data.putAll(data); } } // Omit getter/setter methods @Override public String toString() { return"ErrorReponse{" + "code=" + code + ", status=" + status + ", message='" + message + '\'' + ", path='" + path + '\'' + ", timestamp=" + timestamp + ", data=" + data + '}'; } }
BaseException.java (inherited from the abstract class of RuntimeException, which can be regarded as the parent class of other exception classes in the system)
All exception classes in the system should inherit from this class.
All exception classes in the system should inherit from this class.
publicabstractclass BaseException extends RuntimeException { privatefinal ErrorCode error; privatefinal HashMap<String, Object> data = new HashMap<>(); public BaseException(ErrorCode error, Map<String, Object> data) { super(error.getMessage()); this.error = error; if (!ObjectUtils.isEmpty(data)) { this.data.putAll(data); } } protected BaseException(ErrorCode error, Map<String, Object> data, Throwable cause) { super(error.getMessage(), cause); this.error = error; if (!ObjectUtils.isEmpty(data)) { this.data.putAll(data); } } public ErrorCode getError() { return error; } public Map<String, Object> getData() { return data; } }
ResourceNotFoundException.java (custom exception)
You can see that by inheriting the BaseException class, our custom exception will become very simple!
import java.util.Map; publicclass ResourceNotFoundException extends BaseException { public ResourceNotFoundException(Map<String, Object> data) { super(ErrorCode.RESOURCE_NOT_FOUND, data); } }
GlobalExceptionHandler.java (Global exception capture)
We define two exception capture methods.
Let's explain again. In fact, this class only needs the handleAppException() method, because it is the parent class of all exceptions in this system. Any exception that inherits the BaseException class will be handled here.
import com.twuc.webApp.web.ExceptionController; import org.springframework.http.HttpHeaders; 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 javax.servlet.http.HttpServletRequest; @ControllerAdvice(assignableTypes = {ExceptionController.class}) @ResponseBody publicclass GlobalExceptionHandler { // You can also change BaseException to RuntimeException // Because RuntimeException is the parent of BaseException @ExceptionHandler(BaseException.class) public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) { ErrorReponse representation = new ErrorReponse(ex, request.getRequestURI()); returnnew ResponseEntity<>(representation, new HttpHeaders(), ex.getError().getStatus()); } @ExceptionHandler(value = ResourceNotFoundException.class) public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) { ErrorReponse errorReponse = new ErrorReponse(ex, request.getRequestURI()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorReponse); } }
(important) one expansion:
Ha-ha! In fact, I added an extra exception capture method handleresourcesoftfoundexception() to test you. When we throw a ResourceNotFoundException exception, which of the following methods will catch it?
Answer:
Will be caught by the handleResourceNotFoundException() method. Because the @ ExceptionHandler will find the best match in the process of catching exceptions.
Here is a simple analysis of the source code:
getMappedMethod in ExceptionHandlerMethodResolver.java determines which method to process.
@Nullable private Method getMappedMethod(Class<? extends Throwable> exceptionType) { List<Class<? extends Throwable>> matches = new ArrayList<>(); //Find all exception information that can be handled. The corresponding relationship between exceptions stored in mappedMethods and methods handling exceptions for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) { if (mappedException.isAssignableFrom(exceptionType)) { matches.add(mappedException); } } // Not empty indicates that there is a method to handle exceptions if (!matches.isEmpty()) { // Sort by matching degree from small to large matches.sort(new ExceptionDepthComparator(exceptionType)); // Returns the method to handle the exception returnthis.mappedMethods.get(matches.get(0)); } else { returnnull; } }
From the source code, we can see that getMappedMethod() will first find all the method information that can match the exception handling, then sort it from small to large, and finally take the smallest matching method (that is, the one with the highest matching degree).
Write a class test that throws an exception
Person.java
publicclass Person { private Long id; private String name; // Omit getter/setter methods }
ExceptionController.java (class throwing a field)
@RestController @RequestMapping("/api") publicclass ExceptionController { @GetMapping("/resourceNotFound") public void throwException() { Person p=new Person(1L,"SnailClimb"); thrownew ResourceNotFoundException(ImmutableMap.of("person id:", p.getId())); } }
Source address: https://github.com/snailclam/springboot-guide/tree/master/source-code/basis/springboot-handle-exception-improved