ResponseBodyAdvice
对于控制器:
"/test")
(private static class TestController {
"/hello")
(
public User hello() {
return new User("Tom", 20);
}
}
hello
{"name":"Tom","age":20}
详细调用过程见。
可以通过添加 ResponseBodyAdvice 的方式返回统一格式的结果:
private static class MyControllerAdvice implements ResponseBodyAdvice<Object>{
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result){
return body;
}
return Result.success(body);
}
}
响应:
{"success":true,"message":"","data":{"name":"Tom","age":20}}
实际上用@ControllerAdvice
标记且实现了ResponseBodyAdvice
接口的 Bean 本身就是一个消息转换器,可以作用于处理@ResposeBody
的返回值处理器:
虽然大多数情况下前后端分离的系统中,服务端所有的返回都是 ResponseBody 类型,换言之可以在 ResponseBodyAdvice 的 supports
方法中简单返回 true
,表示包装所有的控制器方法返回值。
但是,如果存在其他形式的返回值,就需要完善supports
方法,让其只处理应该处理的控制器方法返回值:
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 只处理由 @ResponseBOdy 标记的控制器方法
if (returnType.getMethodAnnotation(ResponseBody.class) != null) {
return true;
}
return false;
}
这具有局限性,因为还有可能是方法上没有,但类上使用了@ResponseBody
的情况:
"/test")
(
private static class TestController {
"/tom")
( public User hello() {
return new User("Tom", 20);
}
}
可以添加判断,检查控制器类上是否有该注解:
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 只处理由 @ResponseBOdy 标记的控制器方法
if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
returnType.getContainingClass().getAnnotation(ResponseBody.class) != null) {
return true;
}
return false;
}
但还有可能是使用了组合注解@RestController
替代了@Controller
和ResponseBody
:
"/test")
(
private static class TestController {
"/tom")
( public User hello() {
return new User("Tom", 20);
}
}
因此需要修改为:
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 控制器方法上是否有 @ResponseBody 注解
if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
// 检查类上是否有 @ResponseBody 注解
AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
return true;
}
return false;
}
AnnotationUtils.findAnnotation
方法可以在指定类上查找注解,包括组合注解中的子注解。
注意区分,
junit
包也有个AnnotationUtils
同名类,显然这里应该使用 Spring 包的。
异常处理
在DispatcherServlet
转发请求时,如果产生异常,会由多个HandlerExceptionResolver
负责处理:
其中最常用的一个实现是ExceptionHandlerExceptionResolver
。
假设控制器:
@Controller
private static class TestController {
public void test() {
}
@ExceptionHandler
@ResponseBody
public Map<String, String> handleException(Exception ex) {
return Map.of("error", ex.getMessage());
}
}
@ExceptionHandler
标记的方法handleException
用于处理所在控制器产生的异常,接收的异常类型是Exception
,返回值是Map
类型,且用@ResponseBody
标记,这样可以被返回值处理器处理,并转换为消息后放到响应报文中。
模拟 ExceptionHandlerExceptionResolver
处理由控制器方法 test
产生的异常:
Exception ex = new Exception("test"); // 产生的异常
TestController controller = new TestController(); // 产生异常的控制器
// 产生异常的控制器方法
HandlerMethod handlerMethod = new HandlerMethod(controller, TestController.class.getMethod("test"));
ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver();
// 为异常处理器设置消息转换器
exceptionResolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
// 初始化异常处理器,会添加默认的参数解析器和返回值处理器
exceptionResolver.afterPropertiesSet();
// 模拟请求
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// 处理异常
exceptionResolver.resolveException(request, response, handlerMethod, ex);
System.out.println(response.getContentAsString(StandardCharsets.UTF_8));
对于标准 MVC 设计的服务端,异常处理方法也可以返回 ModelAndView 类型,以便返回渲染的错误页面给前端:
@Controller
private static class TestController {
public void test() {
}
@ExceptionHandler
public ModelAndView handleException(Exception ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("ex", ex);
return modelAndView;
}
}
对应的,测试用例可以:
// 处理异常
ModelAndView modelAndView = exceptionResolver.resolveException(request, response, handlerMethod, ex);
System.out.println(modelAndView.getViewName());
System.out.println(modelAndView.getModel());
特别的,产生的异常可能是多层嵌套的方式(多层异常栈):
Exception ex = new Exception(new RuntimeException(new IOException())); // 产生的异常
处理器方法接收的是内层的异常类型:
@Controller
private static class TestController {
public void test() {
}
@ExceptionHandler
public ModelAndView handleException(IOException ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("ex", ex);
return modelAndView;
}
}
对这种情况,ExceptionHandlerExceptionResolver
依然可以识别,并交给异常处理方法进行处理。
实际上,在ExceptionHandlerExceptionResolver
内部,会将多层异常“拨开”,将其转换为异常数组,然后检查异常处理方法是否能处理其中的某个异常:
在ExceptionHandlerExceptionResolver
初始化的时候,添加的参数解析器中包含ServletRequestMethodArgumentResolver
:
因此异常处理方法可以接收特定类型的参数:
@Controller
private static class TestController {
public void test() {
}
@ExceptionHandler
@ResponseBody
public Map<String, String> handleException(Exception ex, HttpServletRequest request) {
System.out.println(request);
return Map.of("error", ex.getMessage());
}
}
全局异常处理
更常见的是使用全局异常处理:
@Controller
private static class TestController {
public void test() {
}
}
@ControllerAdvice
private static class TestControllerAdvice {
@ExceptionHandler
@ResponseBody
public Map<String, String> handleException(Exception ex, HttpServletRequest request) {
System.out.println(request);
return Map.of("error", ex.getMessage());
}
}
@ControllerAdvice
类中定义的异常处理方法可以处理任意的控制器方法产生的异常:
Exception ex = new Exception("test"); // 产生的异常
TestController controller = new TestController(); // 产生异常的控制器
// 产生异常的控制器方法
HandlerMethod handlerMethod = new HandlerMethod(controller, TestController.class.getMethod("test"));
// 将 ExceptionHandlerExceptionResolver 托管给容器
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
ExceptionHandlerExceptionResolver exceptionResolver = context.getBean(ExceptionHandlerExceptionResolver.class);
// 模拟请求
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
// 处理异常
exceptionResolver.resolveException(request, response, handlerMethod, ex);
System.out.println(response.getContentAsString(StandardCharsets.UTF_8));
配置类:
@Configuration
@Import({TestControllerAdvice.class, TestController.class})
public static class MyConfig{
@Bean
public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() {
ExceptionHandlerExceptionResolver resolver = new ExceptionHandlerExceptionResolver();
resolver.setMessageConverters(List.of(new MappingJackson2HttpMessageConverter()));
// 无需调用 afterPropertiesSet 方法,该方法是生命周期方法,会被框架自动调用
return resolver;
}
}
在 ExceptionHandlerExceptionResolver
的初始化方法afterPropertiesSet
中,有一个initExceptionHandlerAdviceCache
方法:
该方法会从容器中获取 ControllerAdvice 类中的异常处理方法,并进行记录:
这样,控制器方法产生异常且自身没有局部的异常处理方法的时候,就会使用记录的全局差异处理。
Servlet 异常处理
并非所有的异常都能够用 Spring 的异常处理器处理,比如 或 Servlet 中产生的异常。这些异常由 Tomcat 来处理。
先看一个简单,这个示例中,控制器方法中有一个除零异常:
@Controller
@RequestMapping("/my")
public static class MyController {
@GetMapping("/hello")
@ResponseBody
public Result<String> hello() {
return Result.success("hello");
}
@GetMapping("/bye")
@ResponseBody
public Result<String> bye() {
// 人为制造一个异常
int i=1/0;
return Result.success("bye");
}
}
这个异常由全局异常处理捕获和处理:
@ControllerAdvice
public static class MyControllerAdvice {
/**
* 全局异常处理
* @param e 异常
* @return 错误信息
*/
@ExceptionHandler
@ResponseBody
public Result<Void> globalExceptionHandler(Exception e, HttpServletResponse response){
return Result.fail(e.getMessage());
}
}
可以通过访问 查看效果。
如果过滤器中有异常产生:
@Slf4j
@WebFilter("/*")
public static class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) ||
!(response instanceof HttpServletResponse)) {
throw new RuntimeException("这不是一个 HTTP 请求");
}
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI().toString();
log.debug("MyFilter is called.");
// 模拟过滤器存在异常的情况
if (url.equals("/my/hello")){
int i = 1/0;
}
chain.doFilter(request, response);
}
}
Spring 的全局异常处理是无法捕获的,因为过滤器由 Tomcat 而非 Spring 框架管理和执行。
因此执行 看到的是 Tomcat 的错误报告页面:
如果我们需要自定义的 Servlet 错误处理,比如 Servlet Server 产生的错误都以 JSON 格式返回,就需要修改配置类:
/**
* 错误页面注册器,用于为 Tomcat 注册处理错误的页面地址
* @return
*/
@Bean
public ErrorPageRegistrar errorPageRegistrar(){
return new ErrorPageRegistrar() {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage("/error"));
}
};
}
/**
* 为 Tomcat 服务器应用错误页面注册器的 bean 后处理器
* @return
*/
@Bean
public ErrorPageRegistrarBeanPostProcessor errorPageRegistrarBeanPostProcessor(){
return new ErrorPageRegistrarBeanPostProcessor();
}
这里添加了两个 bean:
-
ErrorPageRegistrar,错误页面注册器,可以通过它指定错误页面地址
-
ErrorPageRegistrarBeanPostProcessor,Servlet Server 的 bean 后处理器,可以为其应用错误页面注册器
现在,出现 Servlet 异常后,Servlet Server 会将错误信息以内部转发的方式转发给指定的错误处理页面(这里是/error
)。因此,只要添加对应的错误处理页面的控制器方法就可以正常显示错误信息:
@Controller
@RequestMapping("/error")
public static class MyErrorController {
@GetMapping
@ResponseBody
public Result<Void> error(HttpServletRequest request) {
Throwable error = (Throwable)request.getAttribute((RequestDispatcher.ERROR_EXCEPTION));
return Result.fail(error.getMessage());
}
}
在控制器方法中,可以通过请求域RequestDispatcher.ERROR_EXCEPTION
获取异常信息,Servlet Server 会将异常信息保存在这里。
完整示例看。
BasicErrorController
Spring 提供一个 BasicErrorController
,可以用于 Servlet 异常处理:
这个 Controller 按优先级处理的错误页面地址为:
-
配置项 server.error.path
-
配置项 error.path
-
/error
在配置类中添加:
@Bean
public BasicErrorController basicErrorController(){
return new BasicErrorController(new DefaultErrorAttributes(), new ErrorProperties());
}
需要删除自定义的处理
/error
的控制器方法,否则会有冲突。
这样错误页面就由 BasicErrorController
响应和输出,使用 ApiPost 发起请求会返回:
{
"timestamp": 1751528038778,
"status": 500,
"error": "Internal Server Error",
"path": "/my/hello"
}
可以修改ErrorProperties
以改变输出内容,比如:
@Bean
public BasicErrorController basicErrorController(){
ErrorProperties errorProperties = new ErrorProperties();
errorProperties.setIncludeException(true);
return new BasicErrorController(new DefaultErrorAttributes(), errorProperties);
}
返回结果中会包含异常信息:
{
"timestamp": 1751528445466,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.ArithmeticException",
"path": "/my/hello"
}
BasicErrorController
包含两个控制器方法,一个负责处理 html 请求,一个负责处理其他类型的请求:
如果是 HTML 请求,就会返回 ModelAndView 对象,由模版引擎渲染,使用的视图名称是error
。在这个示例中,缺少error
视图,因此错误页面内容依然是 Servlet 的错误页面。
在这个示例中,米有使用模版引擎,可以直接在配置类中添加 View:
@Bean
public View error() {
return new View() {
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println(model);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().print("""
<h1>服务器内部错误</h1>
<ul>
<li>时间:%s</li>
<li>状态码:%s</li>
<li>错误信息:%s</li>
<li>异常:%s</li>
<li>请求路径:%s</li>
</ul>
""".formatted(model.get("timestamp"),
model.get("status"),
model.get("error"),
model.get("exception"),
model.get("path")));
}
};
}
在其render
方法中,以代码的方式生成了一段 HTML 代码并输出到响应体。
还需要添加一个视图解析器(View Resolver):
@Bean
public ViewResolver viewResolver() {
return new BeanNameViewResolver();
}
BeanNameViewResolver
的用途是可以让 DispatcherServlet
能用视图 Bean 的名称作为视图名称匹配到视图对象。
本文的完整示例可以从获取。
文章评论