在springboot中,可以用统一@ControllerAdvice + @ExceptionHandle,但是无法捕获404异常。如果用浏览器直接访问,可以看到出现以下内容:
image.png
可以看到在404之后,由于没有被@ControllerAdvice捕获,springboot默认会返回/error接口的内容,通过搜索,我们可以在BasicErrorController找到对应的代码。

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

BasicErrorController是默认的异常处理嘞,类里有对error的处理:

@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
   response.setStatus(status.value());
   ModelAndView modelAndView = resolveErrorView(request, response, status, model);
   return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
   Map<String, Object> body = getErrorAttributes(request,
         isIncludeStackTrace(request, MediaType.ALL));
   HttpStatus status = getStatus(request);
   return new ResponseEntity<>(body, status);
}

那我们是不是可以自定义/error处理类呢?
ErrorMvcAutoConfiguration里可以看到BasicErrorController Bean的生成条件:

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
   return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
         this.errorViewResolvers);
}

所以只要我们自定义的异常处理类实现了ErrorController即可。

@Controller
public class NotFoundErrorController implements ErrorController {

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping(value = {"/error"})
    @ResponseBody
    public ResponseEntity<String> error(HttpServletRequest request) {
        return new ResponseEntity("404 Not Found", HttpStatus.OK);
    }
}

如果我们想根据不同的URI做不同的处理,调用request.getRequestURI()却发现方法返回的是/error,那么要怎么取到原始的地址,以及springboot是怎么转到/error的?
通过debug,跟踪到StandardHostValve类的invoke方法里

// Look for (and render if found) an application level error page
if (response.isErrorReportRequired()) {
    // If an error has occurred that prevents further I/O, don't waste time
    // producing an error report that will never be read
    AtomicBoolean result = new AtomicBoolean(false);
    response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
    if (result.get()) {
        if (t != null) {
            throwable(request, response, t);
        } else {
            status(request, response);
        }
    }
}

继续跟进status方法:

/**
 * Handle the HTTP status code (and corresponding message) generated
 * while processing the specified Request to produce the specified
 * Response.  Any exceptions that occur during generation of the error
 * report are logged and swallowed.
 *
 * @param request The request being processed
 * @param response The response being generated
 */
private void status(Request request, Response response) {

    int statusCode = response.getStatus();

    // Handle a custom error page for this status code
    Context context = request.getContext();
    if (context == null) {
        return;
    }

    /* Only look for error pages when isError() is set.
     * isError() is set when response.sendError() is invoked. This
     * allows custom error pages without relying on default from
     * web.xml.
     */
    if (!response.isError()) {
        return;
    }

    ErrorPage errorPage = context.findErrorPage(statusCode);
    if (errorPage == null) {
        // Look for a default error page
        errorPage = context.findErrorPage(0);
    }
    if (errorPage != null && response.isErrorReportRequired()) {
        response.setAppCommitted(false);
        request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,
                          Integer.valueOf(statusCode));

        String message = response.getMessage();
        if (message == null) {
            message = "";
        }
        request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message);
        request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
                errorPage.getLocation());
        request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,
                DispatcherType.ERROR);


        Wrapper wrapper = request.getWrapper();
        if (wrapper != null) {
            request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,
                              wrapper.getName());
        }
        request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,
                             request.getRequestURI());
        if (custom(request, response, errorPage)) {
            response.setErrorReported();
            try {
                response.finishResponse();
            } catch (ClientAbortException e) {
                // Ignore
            } catch (IOException e) {
                container.getLogger().warn("Exception Processing " + errorPage, e);
            }
        }
    }
}
/**
 * Handle an HTTP status code or Java exception by forwarding control
 * to the location included in the specified errorPage object.  It is
 * assumed that the caller has already recorded any request attributes
 * that are to be forwarded to this page.  Return <code>true</code> if
 * we successfully utilized the specified error page location, or
 * <code>false</code> if the default error report should be rendered.
 *
 * @param request The request being processed
 * @param response The response being generated
 * @param errorPage The errorPage directive we are obeying
 */
private boolean custom(Request request, Response response,
                         ErrorPage errorPage) {

    if (container.getLogger().isDebugEnabled()) {
        container.getLogger().debug("Processing " + errorPage);
    }

    try {
        // Forward control to the specified location
        ServletContext servletContext =
            request.getContext().getServletContext();
        RequestDispatcher rd =
            servletContext.getRequestDispatcher(errorPage.getLocation());

        if (rd == null) {
            container.getLogger().error(
                sm.getString("standardHostValue.customStatusFailed", errorPage.getLocation()));
            return false;
        }

        if (response.isCommitted()) {
            // Response is committed - including the error page is the
            // best we can do
            rd.include(request.getRequest(), response.getResponse());
        } else {
            // Reset the response (keeping the real error code and message)
            response.resetBuffer(true);
            response.setContentLength(-1);

            rd.forward(request.getRequest(), response.getResponse());

            // If we forward, the response is suspended again
            response.setSuspended(false);
        }

        // Indicate that we have successfully processed this custom page
        return true;

    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        // Report our failure to process this custom page
        container.getLogger().error("Exception Processing " + errorPage, t);
        return false;
    }
}

可以看到最后是通过forward做跳转,而在跳转之前,通过request.setAttribute将原始请求的属性设置到request里。
所以如果ErrorController里要获取原始的请求信息,可以用request.getAttribute
整体代码如下:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public abstract class UncaughtErrorControllor implements ErrorController {
    @Value("${server.error.path:${error.path:/error}}")
    protected String errorPath;

    @Override
    public String getErrorPath() {
        return errorPath;
    }

    /**
     * @param request
     * @param response
     * @return java.lang.Object
     */
    @RequestMapping(value = "")
    @ResponseBody
    public Object error(HttpServletRequest request, HttpServletResponse response) {
        return handleError(request, response);
    }

    /**
     * @param request
     * @param response
     * @return java.lang.Object
     */
    protected abstract Object handleError(HttpServletRequest request, HttpServletResponse response);
}

public class DefaultUncaughtErrorControllor extends UncaughtErrorControllor {
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    protected Object handleError(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus statusCode = getHttpStatusCode(request);
        String message = buildErrorMessage(request, statusCode);
        log(request, message, statusCode);
        return buildResult(request, message, statusCode);
    }

    /**
     * @param request
     * @return org.springframework.http.HttpStatus
     */
    protected HttpStatus getHttpStatusCode(HttpServletRequest request) {
        try {
            Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
            if (statusCode == null) {
                return HttpStatus.INTERNAL_SERVER_ERROR;
            }
            return HttpStatus.valueOf(statusCode);
        } catch (Exception e) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }

    /**
     * @param request
     * @param statusCode
     * @return java.lang.String
     */
    protected String buildErrorMessage(HttpServletRequest request, HttpStatus statusCode) {
        String uri = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
        return uri + (is404(request, statusCode) ? " Not Found" : " error");
    }

    /**
     * @param request
     * @param statusCode
     * @return boolean
     */
    protected boolean is404(HttpServletRequest reques, HttpStatus statusCode) {
        return statusCode.equals(HttpStatus.NOT_FOUND);
    }

    /**
     * @param request
     * @param message
     * @param statusCode
     */
    protected void log(HttpServletRequest request, String message, HttpStatus statusCode) {
        if (statusCode.is5xxServerError()) {
            logger.error(message);
        } else {
            logger.info(message);
        }
    }

    /**
     * @param request
     * @param message
     * @param statusCode
     * @return java.lang.Object
     */
    protected Object buildResult(HttpServletRequest request, String message, HttpStatus statusCode) {
        return new ResponseEntity(ResponseResult.fail(message), statusCode);
    }
}

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
public class WebConfiguration {
    @Value("${server.error.path:${error.path:/error}}")
    private String errorPath;

    /**
     * @return com.xxx.tscm.web.converter.UncaughtErrorControllor
     */
    @Bean
    @ConditionalOnMissingBean(UncaughtErrorControllor.class)
    public UncaughtErrorControllor getUncaughtErrorControllor() {
        return new DefaultUncaughtErrorControllor();
    }
}

需要注意的事,StandardHostValve对ERROR的处理,是在所有filter(比如spring的OncePerRequestFilter)执行完之后,也就是:即使在自定义error处理方法里,将返回的HTTP Status改为200,但是filter里获取到的还是404。


noname
314 声望49 粉丝

一只菜狗