This article is participating in the Java Theme Month – Java Debug Notes EventActive link

Normally the uniform exception handling we set up in Spring Boot (@RestControllerAdvice with @ExceptionHandler implementation) can only handle exceptions thrown by Controller. Some requests fail before reaching the Controller, and these exceptions cannot be caught by a universal exception, such as some exceptions in the Servlet container. Today I encountered one in project development, which made me very unhappy because the error message format was not uniform, so I decided to find a solution to the problem.

ErrorPageFilter

I believe you are not uncommon in this kind of graph, whenever Spring Boot error, reflected in the page is this. If you get an exception with a test like Postman:

{
  "timestamp": "The 2021-04-29 T22:45-33. 231 + 0000"."status": 500."message": "Internal Server Error"."path": "foo/bar"
}
Copy the code

How does this work? When Spring Boot starts, it registers an ErrorPageFilter. When an exception occurs in the Servlet, the filter will intercept and handle the exception according to different strategies: if the exception is already being handled, it will be handled directly; otherwise, it will be forwarded to the corresponding error page. Interested can go to see the source code, logic is not complex, here is not posted.

In addition, when a Servlet throws an exception, the Servlet handling the exception can get several attributes from HttpServletRequest, as follows:

We can get details about the exception from the above attributes.

Default error page

When Spring Boot fails, it will jump to /error by default, and the related logic of /error is implemented by BasicErrorController.

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    // Return to error page
  @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);
	}
    / / return 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);
	}  
// other omissions
}
Copy the code

The corresponding configuration:

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider
       
         errorViewResolvers)
        {
   return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
         errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
Copy the code

So we just need to re-implement an ErrorController and inject Spring IoC to replace the default handling mechanism. And we can clearly find that this BasicErrorController is not only the implementation of ErrorController but also a controller. If we let the controller method throw exceptions, it can definitely be customized for unified exception handling. So I modified the BasicErrorController:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class ExceptionController extends AbstractErrorController {


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


    @Override
    @Deprecated
    public String getErrorPath(a) {
        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); }}Copy the code

Throw abnormality directly, simple and labor-saving! Most of the exceptions captured here have not gone through the Controller. We also let these exceptions be processed uniformly through the ExceptionController relay to ensure that the exception processing of the whole application maintains a unified facade. Don’t know if you have a better way, welcome to leave a message to discuss.