Usually the unified exception handling we set up in Spring Boot can only handle exceptions thrown by the Controller. Some requests have exceptions before they reach the Controller, and these exceptions cannot be caught by unified exceptions, such as some exceptions in the Servlet container. Today I encountered one in my project development, which irritated me because it returned an error message format that could not be handled uniformly, and I decided to find a solution to this problem.

spring boot error

I’m sure you’ve seen this exception screen many times before, and it’s the default page for Spring Boot whenever there’s an error. If you use a client like Postman to make the request, the exception message is as follows.

1
2
3
4
5
6
{
  "timestamp": "2021-04-29T22:45:33.231+0000",
  "status": 500,
  "message": "Internal Server Error",
  "path": "foo/bar"
}

Spring Boot will register an ErrorPageFilter at startup, and when an exception occurs in the Servlet, the filter will intercept the process and handle the exception according to different strategies: When the exception is already being handled, it is handled directly, otherwise it is forwarded to the corresponding error page. If you are interested in the source code, the logic is not complicated, so I will not post it here.

In addition, when a Servlet throws an exception, the Servlet handling the exception can get several properties from HttpServletRequest inside, as follows.

sobyte

We can get the details of the exception from several properties above.

Default error page

Normally Spring Boot will jump to /error by default to handle exceptions, and the logic for /error is implemented by BasicErrorController.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    //返回错误页面
  @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }
    // 返回json
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }  
// 其它省略
}

And the corresponding configuration.

1
2
3
4
5
6
7
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
      ObjectProvider<ErrorViewResolver> errorViewResolvers) {
   return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
         errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

So we just need to reimplement an ErrorController and inject Spring IoC to replace the default handling mechanism. And we can clearly see that this BasicErrorController is not only an implementation of ErrorController but also a controller, so if we let the controller’s methods throw exceptions, they can definitely be handled by a custom unified exception handling. So I have modified BasicErrorController.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class ExceptionController extends AbstractErrorController {


    public ExceptionController(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }


    @Override
    @Deprecated
    public String getErrorPath() {
        return null;
    }

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(getErrorMessage(request));
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        throw new RuntimeException(getErrorMessage(request));
    }

    private String getErrorMessage(HttpServletRequest request) {
        Object code = request.getAttribute("javax.servlet.error.status_code");
        Object exceptionType = request.getAttribute("javax.servlet.error.exception_type");
        Object message = request.getAttribute("javax.servlet.error.message");
        Object path = request.getAttribute("javax.servlet.error.request_uri");
        Object exception = request.getAttribute("javax.servlet.error.exception");

        return String.format("code: %s,exceptionType: %s,message: %s,path: %s,exception: %s",
                code, exceptionType, message, path, exception);
    }
}

Throwing exceptions directly is simple and effortless! Where most of the exceptions caught here have not gone through the Controller, we let these exceptions be handled uniformly through the ExceptionController relay, ensuring that the exception handling of the entire application maintains a uniform facade to the outside world. I don’t know if you have a better way, welcome to discuss in the comments.

Reference https://felord.cn/spring-boot-error-page.html