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,此处脱敏处理)