在springboot中,可以用统一@ControllerAdvice + @ExceptionHandle
,但是无法捕获404异常。如果用浏览器直接访问,可以看到出现以下内容:
可以看到在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。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。