第三十二讲-Tomcat 异常处理
我们前面讲的@ControllerAdvice配合@ExceptionHandler可以处理异常,但是实际上这套组合处理的异常是非常有限的。为什么这么说呢?如果是Spring MVC框架控制器抛出的异常,那么最终可以是由@ControllerAdvice处理的。但是是如果是Tomcat抛出的异常,例如过滤器抛出的异常,@ControllerAdvice就无法处理了,我们就需要一个更上一层的异常处理者-Tomcat。本讲我们来看看Tomcat异常是如何处理的。
我们首先看看下面的示例代码:
public class A32 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
AnnotationConfigServletWebServerApplicationContext context =
new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
handlerMapping.getHandlerMethods().forEach((RequestMappingInfo k, HandlerMethod v) -> {
System.out.println("映射路径:" + k + "\t方法信息:" + v);
});
}
}
@Configuration
public class WebConfig {
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean // @RequestMapping
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
@Bean // 注意默认的 RequestMappingHandlerAdapter 不会带 jackson 转换器
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
handlerAdapter.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
return handlerAdapter;
}
@Controller
public static class MyController {
@RequestMapping("test")
public ModelAndView test() {
int i = 1 / 0;
return null;
}
}
}
我们来测试一下:
映射路径:{ [/test]} 方法信息:com.cherry.a32.WebConfig$MyController#test()
此时有一个路径映射,接着我们用浏览器来访问一下:
我们发现,这个错误异常页面是Tomcat提供的。而且还是html格式的。但是呢,在开发过程中,我们有时候会将错误信息以JSON的格式返回个客户端,那么怎么定制错误响应内容呢?我们来尝试编写以下。
首先我们添加一个Bean,该Bean用于指定Tomcat在运行过程中如果出错了,发生错误的处理路径是什么:
@Bean // 修改 Tomcat 服务器默认错误地址
public ErrorPageRegistrar errorPageRegistrar() { // 出现错误,会使用请求转发 forward 跳转到 error 地址
return webServerFactory -> webServerFactory.addErrorPages(new ErrorPage("/error"));
}
再添加一个BeanPostProcessor,专门用于处理错误页面Bean的Bean后处理器:
@Bean
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor() {
return new ErrorPageRegistrarBeanPostProcessor();
}
添加一个控制器映射,专门用于处理Tomcat发生错误时使用的控制器
@RequestMapping("/error")
@ResponseBody
public Map
Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
return Map.of("error", e.getMessage());
}
成功启动测试:
映射路径:{ [/error]} 方法信息:com.cherry.a32.WebConfig$MyController#error(HttpServletRequest)
映射路径:{ [/test]} 方法信息:com.cherry.a32.WebConfig$MyController#test()
这样就把我们Tomcat错误页面做成了自定义,让错误信息以JSON格式返回给了浏览器。
我们讲了Tomcat的异常处理,就不得不提Spring Boot中的BasicErrorController这个类,这个类的作用和我们刚才写的error控制器是一样的,都是为了配合Tomcat错误路径作出相应的处理。
我们可以看一下BasicErrorController
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
// ......
// 可以返回HTML格式的异常信息
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map
.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
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map
return new ResponseEntity<>(body, status);
}
// ....
}
这个BasicErrorController也是一个控制器,其路径默认是从配置文件中读取server.error.path参数,如果该参数为空,会读取error.path参数,如果这个参数还未空,就是使用/error路径,我们发现这个BasicErrorController和我们刚才写的错误控制器有着异曲同工之处,较为相似。
下面呢,我们将我们自己写的错误控制器注释掉,使用BasicErrorController来写一个错误处理控制器:
// @RequestMapping("/error")
// @ResponseBody
// public Map
// Throwable e = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
// return Map.of("error", e.getMessage());
// }
// }
@Bean
public BasicErrorController basicErrorController() {
ErrorProperties errorProperties = new ErrorProperties();
// 返回的信息包含错误信息
errorProperties.setIncludeException(true);
// 需要错误属性(例如错误发生的时间,错误路径,异常等信息)和配置文件信息(从配置文件中读取错误相关的配置信息)
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}
我们重启重新测试一下:
映射路径:{ [/error]} 方法信息:org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
映射路径:{ [/test]} 方法信息:com.cherry.a32.WebConfig$MyController#test()
映射路径:{ [/error], produces [text/html]} 方法信息:org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
我们发现,异常类是由Spring框架提供的。那么我们如何返回自定义格式的相关错误信息呢?我们可以自己设定错误视图对象/error。
我们手动来创建一个view对象(注意视图名字最好奇error,否则起其它名字最好在配置文件中指定错误视图的名字)
@Bean
public View error() {
return new View() {
@Override
public void render(Map
System.out.println(model);
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("""
服务器内部错误
""");
}
};
}
// 创建一个视图解析器,用于解析我们自己创建的视图
@Bean
public ViewResolver viewResolver() {
return new BeanNameViewResolver();
}
重新启动Tomcat,测试一下:
除此之外,我们也可以照葫芦画瓢可以做出更加丰富的额扩展,例如返回JSON格式的异常数据、XML格式的异常数据等等。这里呢,就不再一一演示,办法也很简单,只需要将自定义的错误视图的响应类型做相应的修改即可以实现。