1. 概述

本文将带你掌握如何在 Spring 的 @RestController 中正确抛出异常,并使用 MockMvc 对这些异常进行单元测试。内容虽短,但覆盖了实际开发中常见的异常处理与验证场景,适合快速查阅和踩坑参考。

2. 在控制器中抛出异常

我们先从最基础的开始:如何从一个控制器方法中直接抛出异常。

可以把控制器暴露的接口看作普通的 Java 方法,支持直接 throw 异常:

@GetMapping("/exception/throw")
public void getException() throws Exception {
    throw new Exception("error");
}

当我们调用这个接口时,会观察到两个关键现象:

✅ 响应状态码是 500 Internal Server Error
✅ 返回体中包含了异常堆栈信息,格式如下:

{
    "timestamp": 1592074599854,
    "status": 500,
    "error": "Internal Server Error",
    "message": "No message available",
    "trace": "java.lang.Exception
              at com.baeldung.controllers.ExceptionController.getException(ExceptionController.java:26)
              ..."
}

⚠️ 注意:直接抛出 Exception 会导致返回 500 错误,并暴露堆栈,在生产环境属于敏感信息泄露,不推荐这么做。

结论:在 @RestController 中抛出未处理的异常,默认会被 Spring 映射为 500 错误,且响应体包含详细堆栈 —— 这对调试有用,但上线前务必通过统一异常处理(如 @ControllerAdvice)屏蔽细节。

3. 将异常映射为指定 HTTP 状态码

为了更精确地控制错误响应(比如参数错误返回 400,资源不存在返回 404),我们可以使用 Spring 提供的 @ResponseStatus 注解来自定义异常。

自定义异常类

通过在异常类上添加 @ResponseStatus,可以指定该异常触发时返回的 HTTP 状态码:

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadArgumentsException extends RuntimeException {
    public BadArgumentsException(String message) {
        super(message);
    }
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class InternalException extends RuntimeException {
    public InternalException(String message) {
        super(message);
    }
}
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

控制器中使用自定义异常

接下来在控制器中根据参数抛出对应的异常:

@GetMapping("/exception/{exception_id}")
public void getSpecificException(@PathVariable("exception_id") String pException) {
    if ("not_found".equals(pException)) {
        throw new ResourceNotFoundException("resource not found");
    } else if ("bad_arguments".equals(pException)) {
        throw new BadArgumentsException("bad arguments");
    } else {
        throw new InternalException("internal error");
    }
}

调用结果验证:

参数值 预期状态码 说明
not_found 404 Not Found ✅ 匹配 @ResponseStatus(HttpStatus.NOT_FOUND)
bad_arguments 400 Bad Request ✅ 映射到 BAD_REQUEST
其他任意值 500 Internal Server Error ✅ 默认走 InternalException

返回体结构与之前一致,但状态码更语义化,便于前端判断错误类型。

4. 使用 MockMvc 测试异常

真正的生产级代码必须有测试保障。下面我们用 Spring 的 MockMvc 来验证控制器是否正确抛出了预期异常。

准备测试环境

首先注入 MockMvc 实例(通常配合 @WebMvcTest@SpringBootTest 使用):

@Autowired
private MockMvc mvc;

编写测试用例

每个测试用例都应验证三点:

  • ✅ HTTP 状态码是否正确
  • ✅ 抛出的异常类型是否符合预期
  • ✅ 异常消息内容是否匹配
@Test
public void givenNotFound_whenGetSpecificException_thenNotFoundCode() throws Exception {
    String exceptionParam = "not_found";

    mvc.perform(get("/exception/{exception_id}", exceptionParam)
          .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isNotFound())
        .andExpect(result -> assertTrue(result.getResolvedException() instanceof ResourceNotFoundException))
        .andExpect(result -> assertEquals("resource not found", result.getResolvedException().getMessage()));
}
@Test
public void givenBadArguments_whenGetSpecificException_thenBadRequest() throws Exception {
    String exceptionParam = "bad_arguments";

    mvc.perform(get("/exception/{exception_id}", exceptionParam)
          .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isBadRequest())
        .andExpect(result -> assertTrue(result.getResolvedException() instanceof BadArgumentsException))
        .andExpect(result -> assertEquals("bad arguments", result.getResolvedException().getMessage()));
}
@Test
public void givenOther_whenGetSpecificException_thenInternalServerError() throws Exception {
    String exceptionParam = "dummy";

    mvc.perform(get("/exception/{exception_id}", exceptionParam)
          .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isInternalServerError())
        .andExpect(result -> assertTrue(result.getResolvedException() instanceof InternalException))
        .andExpect(result -> assertEquals("internal error", result.getResolvedException().getMessage()));
}

关键 API 解析

  • result.getResolvedException():获取被 Spring 处理后的实际抛出异常对象
  • status().isXXX():断言响应状态码
  • Lambda 断言:可直接对异常实例做类型和消息验证,简单粗暴有效

✅ 这种方式绕过了 JSON 响应体解析,直接检查内部异常,更加精准高效。

5. 总结

本文总结了 Spring Web 中异常处理与测试的核心实践:

  • ❌ 避免直接抛 Exception,会导致 500 + 暴露堆栈
  • ✅ 使用 @ResponseStatus 自定义异常,实现语义化错误码
  • ✅ 利用 MockMvc 结合 getResolvedException() 精确验证异常类型、消息和状态码
  • ⚠️ 实际项目建议配合 @ControllerAdvice 做全局异常统一处理,避免重复代码

示例完整代码已托管至 GitHub:https://github.com/example/spring-rest-testing(原项目为 baeldung/tutorials,此处脱敏处理)


原始标题:Testing Exceptions with Spring MockMvc