红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 13:处理响应和异常

2025年7月3日 8点热度 0人点赞 0条评论

ResponseBodyAdvice

对于控制器:

@Controller
@RequestMapping("/test")
private static class TestController {
    @GetMapping("/hello")
    @ResponseBody
    public User hello() {
        return new User("Tom", 20);
    }
}

hello方法调用后的响应体:

{"name":"Tom","age":20}

详细调用过程见源码。

可以通过添加 ResponseBodyAdvice 的方式返回统一格式的结果:

@ControllerAdvice
private static class MyControllerAdvice implements ResponseBodyAdvice<Object>{
​
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }
​
    @Override
    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的返回值处理器:

image-20250702185315786

虽然大多数情况下前后端分离的系统中,服务端所有的返回都是 ResponseBody 类型,换言之可以在 ResponseBodyAdvice 的 supports 方法中简单返回 true,表示包装所有的控制器方法返回值。

但是,如果存在其他形式的返回值,就需要完善supports方法,让其只处理应该处理的控制器方法返回值:

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    // 只处理由 @ResponseBOdy 标记的控制器方法
    if (returnType.getMethodAnnotation(ResponseBody.class) != null) {
        return true;
    }
    return false;
}

这具有局限性,因为还有可能是方法上没有,但类上使用了@ResponseBody的情况:

@Controller
@RequestMapping("/test")
@ResponseBody
private static class TestController {
    @GetMapping("/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 ||
            returnType.getContainingClass().getAnnotation(ResponseBody.class) != null) {
        return true;
    }
    return false;
}

但还有可能是使用了组合注解@RestController替代了@Controller和ResponseBody:

@RequestMapping("/test")
@RestController
private static class TestController {
    @GetMapping("/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负责处理:

image-20250702194720173

其中最常用的一个实现是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内部,会将多层异常“拨开”,将其转换为异常数组,然后检查异常处理方法是否能处理其中的某个异常:

image-20250702202526606

在ExceptionHandlerExceptionResolver初始化的时候,添加的参数解析器中包含ServletRequestMethodArgumentResolver:

image-20250702203147492

因此异常处理方法可以接收特定类型的参数:

@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方法:

image-20250702205208322

该方法会从容器中获取 ControllerAdvice 类中的异常处理方法,并进行记录:

image-20250702205403689

这样,控制器方法产生异常且自身没有局部的异常处理方法的时候,就会使用记录的全局差异处理。

Servlet 异常处理

并非所有的异常都能够用 Spring 的异常处理器处理,比如 过滤器(Filter) 或 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());
    }
}

可以通过访问 localhost:8080/my/bye 查看效果。

如果过滤器中有异常产生:

@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 框架管理和执行。

因此执行 HTTP Status 500 – Internal Server Error 看到的是 Tomcat 的错误报告页面:

image-20250703141349895

如果我们需要自定义的 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 异常处理:

image-20250703153554498

这个 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 请求,一个负责处理其他类型的请求:

image-20250703154820168

如果是 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 的名称作为视图名称匹配到视图对象。

本文的完整示例可以从这里获取。

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: spring boot 异常处理 统一返回
最后更新:2025年7月3日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号