Revisit the unified global exception handling of the SpringBoot series
- Design an excellent exception handling mechanism
- Custom exceptions and related data structures
- General global exception handling logic
- Server data verification exception handling logic
- AOP perfectly handles page Jump exceptions
Design an excellent exception handling mechanism
Examples of abnormal handling
Chaos 1: only output to the console after capturing exceptions
Front end JS Ajax code
$.ajax({ type: "GET", url: "/user/add", dataType: "json", success: function(data){ alert("Added successfully"); } });
Back end business code
try { // do something } catch (XyyyyException e) { e.printStackTrace(); }
Question:
- The back end directly captures exceptions and only prints logs. The user experience is very poor. Once there is an error in the background, the user has no perception and the page is stateless.
- The back-end only gives the front-end exception result, and does not give a description of the cause of the exception. The user does not know whether it is his own operation input error or system bug. Users can't judge whether they need to operate later? Or continue to the next step?
- If no one pays regular attention to the server log, no one will find any exceptions in the system.
Chaos II: chaotic return mode
Front end code
$.ajax({ type: "GET", url: "/goods/add", dataType: "json", success: function(data) { if (data.flag) { alert("Added successfully"); } else { alert(data.message); } }, error: function(data){ alert("Add failed"); } });
Back end code
@RequestMapping("/goods/add") @ResponseBody public Map add(Goods goods) { Map map = new HashMap(); try { // do something map.put(flag, true); } catch (Exception e) { e.printStackTrace(); map.put("flag", false); map.put("message", e.getMessage()); } reutrn map; }
Question:
- The data returned by each person has its own specification. Your name is flag and his name is isOK. Your success code is 0 and its success code is 0000. This leads to a large number of exception return logic codes written on the back end, and a set of exception handling logic for each request on the front end. A lot of duplicate code.
- If the front-end and back-end are developed by one person, they can barely be used. If the front-end and back-end are separated, it is a system disaster.
How to design exception handling

Friendly to interested parties
- Back end developers have a single responsibility. They only need to capture and convert exceptions into custom exceptions and throw them all the time. There is no need to think about the design of page Jump 404 and the data structure of abnormal response.
- It is friendly to the front-end personnel. The data returned by the back-end to the front-end should have a unified data structure and unified specifications. A data structure that cannot respond to one person. In this process, there is no need for back-end developers to do more work and give it to the global exception handler to handle the conversion from "exception" to "response data structure".
- User friendly, users can clearly know the cause of exceptions. This requires custom exceptions, unified global processing, ajax interface request response to unified exception data structure, and page template request to jump to 404 page
- For operation and maintenance friendliness, the abnormal information is reasonably and regularly persisted and stored in the form of log for query.
Why do you want to convert system runtime exception capture to custom exception throw?
A: because the user doesn't know what an exception like ConnectionTimeOutException is, but converting to a custom exception requires the programmer to translate the runtime exception. For example, there should be a message field in the custom exception, and the back-end programmer should clearly use a user-oriented friendly language in the message field to explain what happened on the server.
Development specification
- Exceptions intercepted by Controller, Service and DAO layers are converted into custom exceptions, and exceptions are not allowed to be intercepted privately. Must be thrown out.
- Unified data response code, use HTTP status code, do not customize. Custom inconvenient memory, HTTP status code programmers know. But too many programmers can't remember. Just use a few within the scope specified by the project team. For example: 200 successful requests, 400 exceptions caused by user input errors, 500 system internal exceptions, and 999 unknown exceptions.
- There is a message attribute in the custom exception, which describes the occurrence of the exception in user-friendly language and assigns it to message
- It is not allowed to uniformly catch the parent Exception. It should be divided into subclasses of catch, so that the Exception can be clearly converted into a user-defined Exception and passed to the front end.
Custom exceptions and related data structures
How to design data structure
- CustomException custom exception. The core elements include exception error code (400500) and exception error message.
- ExceptionTypeEnum enumerates exception classifications to solidify exception classifications and prevent developers from thinking divergently.
- Ajax response is a unified data structure used to respond to HTTP requests.
Enumerating the types of exceptions
In order to prevent the brain divergence of developers, each developer constantly invents his own exception type. We need to specify the exception type (enumeration). For example, system exceptions, exceptions caused by user (input) operations, other exceptions, etc.
public enum CustomExceptionType { USER_INPUT_ERROR(400,"The data you entered is incorrect or you do not have permission to access resources!"), SYSTEM_ERROR (500,"The system is abnormal. Please try again later or contact the administrator!"), OTHER_ERROR(999,"Unknown exception occurred in the system, please contact the administrator!"); CustomExceptionType(int code, String desc) { this.code = code; this.desc = desc; } private String desc;//Chinese description of exception type private int code; //code public String getDesc() { return desc; } public int getCode() { return code; } }
- In the author's experience, it's best not to exceed 5, otherwise developers will not remember and are unwilling to remember. For me, the above three exception types are enough.
- The code here represents the unique code of the exception type. In order to facilitate everyone's memory, Http status codes 400 and 500 are used
- The desc here is a general exception description. When creating a custom exception, in order to give a more friendly reply to the user, the exception information description should be more specific and friendly.
Custom exception
- Custom exceptions have two core contents, one is code. Use CustomExceptionType to qualify the scope.
- The other is message, which is to be returned to the front end at last, so you need to use friendly prompts to express the cause or content of the exception
public class CustomException extends RuntimeException { //Exception error code private int code ; //Abnormal information private String message; private CustomException(){} public CustomException(CustomExceptionType exceptionTypeEnum) { this.code = exceptionTypeEnum.getCode(); this.message = exceptionTypeEnum.getDesc(); } public CustomException(CustomExceptionType exceptionTypeEnum, String message) { this.code = exceptionTypeEnum.getCode(); this.message = message; } public int getCode() { return code; } @Override public String getMessage() { return message; } }
Request interface unified response data structure
In order to solve the problem that different developers use different structures to respond to the front end, resulting in inconsistent specifications and chaotic development. We use the following code to define the unified data response structure
- isok indicates whether the request was processed successfully (i.e. whether an exception occurred). true indicates that the request was processed successfully, and false indicates that the processing failed.
- code further refines the response results. 200 indicates that the request is successful, 400 indicates the exception caused by the user's operation, 500 indicates the system exception, and 999 indicates other exceptions. Consistent with CustomExceptionType enumeration.
- Message: friendly prompt message, or request result prompt message. If the request succeeds, this information is usually useless. If the request fails, this information needs to be displayed to the user. Data: usually used to query data requests. After successful, the query data is responded to the front end.
/** * Interface data request unified response data structure */ @Data public class AjaxResponse { private boolean isok; //Is the request processed successfully private int code; //Request response status code private String message; //Request result description information private Object data; //Request result data (usually used for query operations) private AjaxResponse(){} //Response data encapsulation in case of request exception public static AjaxResponse error(CustomException e) { AjaxResponse resultBean = new AjaxResponse(); resultBean.setIsok(false); resultBean.setCode(e.getCode()); resultBean.setMessage(e.getMessage()); return resultBean; } //Response data encapsulation in case of request exception public static AjaxResponse error(CustomExceptionType customExceptionType, String errorMessage) { AjaxResponse resultBean = new AjaxResponse(); resultBean.setIsok(false); resultBean.setCode(customExceptionType.getCode()); resultBean.setMessage(errorMessage); return resultBean; } //A successful response to the request without query data (used to delete, modify and add interfaces) public static AjaxResponse success(){ AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage("Request response succeeded!"); return ajaxResponse; } //A successful response with query data (for the data query interface) public static AjaxResponse success(Object obj){ AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage("Request response succeeded!"); ajaxResponse.setData(obj); return ajaxResponse; } //A successful response with query data (for the data query interface) public static AjaxResponse success(Object obj,String message){ AjaxResponse ajaxResponse = new AjaxResponse(); ajaxResponse.setIsok(true); ajaxResponse.setCode(200); ajaxResponse.setMessage(message); ajaxResponse.setData(obj); return ajaxResponse; } }
For different scenarios, there are four ways to build Ajax response.
- When the request is successful, you can use ajax response. Success () to build the returned result to the front end.
- When the query request needs to return business data, and the request is successful, you can use ajax response. Success (data) to build the return result to the front end. Carry the result data.
- When an exception occurs during request processing, you need to convert the exception into a CustomException, and then use ajax response. Error (CustomException) in the control layer to build the returned result to the front end.
- In some cases, there is no exception. We judge that some conditions also consider the request failed. This uses Ajax response. Error (customexception type, ErrorMessage) to build the response result.
Use examples are as follows
For example, in the update operation, the Controller does not need to return additional data
return AjaxResponse.success();

For example, when querying an interface, the Controller needs to return result data (data can be any type of data)
return AjaxResponse.success(data);
General global exception handling logic
General exception handling logic
The programmer's exception handling logic should be very single: no matter in the Controller layer, Service layer or any other location, the programmer is only responsible for one thing: capturing exceptions and converting exceptions into custom exceptions. Use user-friendly information to fill in the message of CustomException and throw out CustomException.
@Service public class ExceptionService { //Service layer, simulation system exception public void systemBizError() { try { Class.forName("com.mysql.jdbc.xxxx.Driver"); } catch (ClassNotFoundException e) { throw new CustomException( CustomExceptionType.SYSTEM_ERROR, "stay XXX Business, myBiz()Within the method, the ClassNotFoundException,Please inform the administrator of this information"); } } //The service layer simulates verification exceptions caused by user input data public void userBizError(int input) { if(input < 0){ //Analog service verification failure logic throw new CustomException( CustomExceptionType.USER_INPUT_ERROR, "The data you entered does not conform to business logic, please confirm and re-enter!"); } //... other business } }
Global exception handler
Through the requirements of the coding specification within the team, we already know that programmers are not allowed to intercept and handle exceptions. Exceptions must be converted into custom exceptions, and all customexceptions must be thrown out. So who will handle the Exception after the programmer runs it out? That's controller advice.
The role of the ControllerAdvice annotation is to listen to all controllers. Once the Controller throws a CustomException, it will handle the exception in the method of the @ ExceptionHandler(CustomException.class) annotation.
The processing method is very simple, that is, it uses Ajax response. Error (E) to wrap it as a general interface data structure and return it to the front end.
@ControllerAdvice public class WebExceptionHandler { //Handle user-defined exceptions actively converted by programmers @ExceptionHandler(CustomException.class) @ResponseBody public AjaxResponse customerException(CustomException e) { if(e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()){ //400 exceptions do not need to be persisted. You can inform the user of the exception information in a friendly way //TODO will persist 500 abnormal information for the convenience of operation and maintenance personnel } return AjaxResponse.error(e); } //The handler failed to catch (omitted) exceptions in the program @ExceptionHandler(Exception.class) @ResponseBody public AjaxResponse exception(Exception e) { //TODO will persist the abnormal information for the convenience of operation and maintenance personnel return AjaxResponse.error(new CustomException( CustomExceptionType.OTHER_ERROR)); } }

The business status is consistent with the HTTP protocol status
I wonder if you have noticed a problem (see the picture above)? The problem is that the code of our Ajax response is 400, but the real HTTP protocol status code is 200? Generally speaking, at present
- The code of Ajax response is 400, which represents the business status, that is, the user's request for business failed
- However, the HTTP request is successful, that is, the data is returned normally.
When many companies develop RESTful services, the HTTP status code is required to reflect the final execution status of the business. Therefore, it is necessary to make the business status consistent with the Response status code of the HTTP protocol.
@Component @ControllerAdvice public class GlobalResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { //return returnType.hasMethodAnnotation(ResponseBody.class); return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { //If the response result is JSON data type if(selectedContentType.equalsTypeAndSubtype( MediaType.APPLICATION_JSON)){ //Set the status code for the HTTP response result. The status code is the code of Ajax response. The two are unified response.setStatusCode( HttpStatus.valueOf(((AjaxResponse) body).getCode()) ); return body; } return body; } }
- The function of implementing the ResponseBodyAdvice interface is to do the last step before returning the data to the user. That is, the processing procedure of ResponseBodyAdvice follows the global exception handling.

Further optimization
We already know that the ResponseBodyAdvice interface is used to do the last step before returning the data to the user. Optimize the beforeBodyWrite method code in GlobalResponseAdvice above as follows.

- If the result body of the Controller or global exception handling response is Ajax response, return it directly to the front end.
- If the result body of the Controller or global exception handling response is not an Ajax response, encapsulate the body as an Ajax response and return it to the front end.
Therefore, our previous code is written like this, for example, the return value of a controller method
return AjaxResponse.success(objList);
It can be written like this now, because the global response advice will be uniformly encapsulated as Ajax response.
return objList;
The final code is as follows:
@Component @ControllerAdvice public class GlobalResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter methodParameter, Class aClass) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { //If the response result is JSON data type if(mediaType.equalsTypeAndSubtype( MediaType.APPLICATION_JSON)){ if(body instanceof AjaxResponse){ AjaxResponse ajaxResponse = (AjaxResponse)body; if(ajaxResponse.getCode() != 999){ //999 is not a standard HTTP status code. Special processing is required serverHttpResponse.setStatusCode(HttpStatus.valueOf( ajaxResponse.getCode() )); } return body; }else{ serverHttpResponse.setStatusCode(HttpStatus.OK); return AjaxResponse.success(body); } } return body; } }
Server data verification exception handling logic
Specification and common notes of exception verification
In web development, the validity of request parameters generally needs to be verified. In the original writing method, it is judged field by field. This method is too generic, so the JSR 303: Bean Validation specification of java solves this problem.
JSR 303 is only a specification with no specific implementation. At present, hibernate validator is usually used for unified parameter verification.
Verification class defined by JSR303

constraint attached to Hibernate Validator

Usage: add the above annotation to the attribute field of ArticleVO, and then add @ Valid annotation to the parameter verification method, such as:


BindException or MethodArgumentNotValidException will be thrown when the user input parameters do not comply with the verification rules given by the annotation.
Assert assertion and IllegalArgumentException
Before I told you about general exception handling, the user input exception judgment is handled in this way. This method can also be used, but we have learned so much knowledge that we can optimize it
//The service layer simulates verification exceptions caused by user input data public void userBizError(int input) { if(input < 0){ //Analog service verification failure logic throw new CustomException( CustomExceptionType.USER_INPUT_ERROR, "The data you entered does not conform to business logic, please confirm and re-enter!"); } //... other business }
A better way to write it is as follows. Use org.springframework.util.Assert to assert input > = 0. If the conditions are not met, throw an IllegalArgumentException with illegal parameters.
//The service layer simulates verification exceptions caused by user input data public void userBizError(int input) { Assert.isTrue(input >= 0,"The data you entered does not conform to business logic, please confirm and re-enter!"); //... other business }
org.springframework.util.Assert assertion provides a large number of assertion methods to verify the validity of data for various data types. It is more convenient for us to write code.
Friendly data verification exception handling (Global handling of user input exceptions)
We know that when data verification fails, an exception BindException or MethodArgumentNotValidException will be thrown. Therefore, we do global processing for these two exceptions to prevent the trouble caused by repeated coding by programmers.
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseBody public AjaxResponse handleBindException(MethodArgumentNotValidException ex) { FieldError fieldError = ex.getBindingResult().getFieldError(); return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); } @ExceptionHandler(BindException.class) @ResponseBody public AjaxResponse handleBindException(BindException ex) { FieldError fieldError = ex.getBindingResult().getFieldError(); return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR, fieldError.getDefaultMessage())); }
We know to use org.springframework.util.Assert assertion to throw IllegalArgumentException if the condition is not met. You can use the following global exception handling functions
@ExceptionHandler(IllegalArgumentException.class) @ResponseBody public AjaxResponse handleIllegalArgumentException(IllegalArgumentException e) { return AjaxResponse.error( new CustomException(CustomExceptionType.USER_INPUT_ERROR, e.getMessage()) ); }
AOP perfectly handles page Jump exceptions
Page Jump exception handling
The previous chapters talked about exception handling of JSON interface classes. What should we do if an exception occurs in the Controller when we develop a page template (not a front-end and back-end separated application)? You should jump to the error.html page in a unified manner, and you should not affect the global unified exception handling of the JSON data interface.
- Problems faced:
The programmer throws a custom exception (single responsibility). After the global exception handling is intercepted, it returns @ ResponseBody Ajax response instead of ModelAndView, so we can't jump to the error.html page. How can we do the global exception handling of page Jump error.html?

Here is my answer:
- Convert the Exception to ModelAndViewException in a faceted manner.
- The global exception handler intercepts the ModelAndViewException and returns ModelAndView, that is, the error.html page
- The pointcut is the Controller layer method annotated with @ ModelView
Using this method to handle page class exceptions, programmers only need to add @ ModelView annotation to the Controller method involving page Jump. When the method throws an exception, it will automatically jump to the error page.
Wrong writing
@GetMapping("/freemarker") public String index(Model model) { try{ List<ArticleVO> articles = articleRestService.getAll(); model.addAttribute("articles", articles); }catch (Exception e){ return "error"; } return "fremarkertemp"; }
Correct writing
@ModelView @GetMapping("/freemarker") public String index(Model model) { List<ArticleVO> articles = articleRestService.getAll(); model.addAttribute("articles", articles); return "fremarkertemp"; }
Dealing with page global exceptions with aspect oriented method
Because aspect oriented programming is used, maven dependency package is introduced
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
ModelView annotation is only used for annotation
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD})//This annotation can only be used on methods public @interface ModelView { }
Take the @ ModelView annotation as the starting point, face-to-face programming, and convert all captured exceptions into modelviewexceptions.
@Aspect @Component @Slf4j public class ModelViewAspect { //Set pointcut: methods annotated by @ ModelView are directly intercepted here @Pointcut("@annotation(com.dhy.boot.launch.exception.ModelView)") public void pointcut() { } /** * When an exception is thrown by a method annotated with ModelView, do the following */ @AfterThrowing(pointcut="pointcut()",throwing="e") public void afterThrowable(Throwable e) { throw ModelViewException.transfer(e); } }
A new Exception class ModelViewException is defined to convert the caught Exception into ModelViewException
public class ModelViewException extends RuntimeException{ //Convert Exception to ModelViewException public static ModelViewException transfer(Throwable cause) { return new ModelViewException(cause); } private ModelViewException(Throwable cause) { super(cause); } }
The global exception handler handles the ModelViewException and locates the exception page to error.html:
@ExceptionHandler(ModelViewException.class) public ModelAndView viewExceptionHandler(HttpServletRequest req, ModelViewException e) { ModelAndView modelAndView = new ModelAndView(); //Set exception information, such as modelAndView modelAndView.addObject("exception", e); modelAndView.addObject("url", req.getRequestURL()); modelAndView.setViewName("error"); //Return ModelAndView return modelAndView; }
Access test
Write an error page. Because I use the freemaker template, it is error.ftl (if there is no template engine, error.html is OK).
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8" /> <title>error.html</title> </head> <body> <h1>exception.toString()</h1> <div>${exception.toString()}</div> <h1>exception.message</h1> <div>${exception.message}</div> <h1>url</h1> <div>${url}</div> </body> </html>
Just find a controller method for page Jump. I visited the previously developed method http://localhost:8888/template/freemarker Test and artificially create an exception before accessing. It is important not to forget to add the @ ModelView annotation

The results of the visit are as follows. Go to the error.html page (my error page is simple, and you can customize the style):
