跳到主要内容

异常处理

DeepSeek V3 中英对照 Exceptions

@Controller@ControllerAdvice 类可以包含 @ExceptionHandler 方法,用于处理来自控制器方法的异常,如下例所示:

import java.io.IOException;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;

@Controller
public class SimpleController {

@ExceptionHandler(IOException.class)
public ResponseEntity<String> handle() {
return ResponseEntity.internalServerError().body("Could not read file storage");
}

}
java

异常映射

异常可能匹配正在传播的顶级异常(例如,直接抛出的 IOException)或包装异常中的嵌套原因(例如,包装在 IllegalStateException 中的 IOException)。从 5.3 版本开始,这可以在任意原因级别上进行匹配,而之前仅考虑直接原因。

为了匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,通常优先选择根异常匹配,而不是原因异常匹配。更具体地说,ExceptionDepthComparator 用于根据异常与抛出异常类型的深度来排序异常。

或者,注解声明可以缩小异常类型以匹配,如下例所示:

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handleIoException(IOException ex) {
return ResponseEntity.internalServerError().body(ex.getMessage());
}
java

你甚至可以使用具有非常通用的参数签名的特定异常类型列表,如下例所示:

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handleExceptions(Exception ex) {
return ResponseEntity.internalServerError().body(ex.getMessage());
}
java
备注

根异常和原因异常匹配之间的区别可能会让人感到意外。

在前面的 IOException 变体中,该方法通常以实际的 FileSystemExceptionRemoteException 实例作为参数调用,因为它们都继承自 IOException。然而,如果任何此类匹配的异常在包装异常中传播,而该包装异常本身也是一个 IOException,那么传入的异常实例就是该包装异常。

handle(Exception) 变体中,行为更加简单。在包装场景中,总是使用包装异常调用该方法,而实际匹配的异常需要通过 ex.getCause() 来找到。传入的异常只有在这些异常作为顶级异常抛出时,才是实际的 FileSystemExceptionRemoteException 实例。

我们通常建议您在参数签名中尽可能具体,以减少根异常类型与原因异常类型之间不匹配的可能性。考虑将一个多匹配方法拆分为多个单独的 @ExceptionHandler 方法,每个方法通过其签名匹配一个特定的异常类型。

在多 @ControllerAdvice 配置中,我们建议在具有相应优先级的 @ControllerAdvice 上声明您的主要根异常映射。虽然根异常匹配优先于原因匹配,但这在给定控制器或 @ControllerAdvice 类的方法中定义。这意味着在优先级较高的 @ControllerAdvice bean 上的原因匹配优先于在优先级较低的 @ControllerAdvice bean 上的任何匹配(例如,根匹配)。

最后但同样重要的是,@ExceptionHandler 方法实现可以选择通过以原始形式重新抛出异常来处理给定的异常实例。这在您仅对根级别匹配或无法静态确定的特定上下文中的匹配感兴趣的情况下非常有用。重新抛出的异常会通过剩余的解析链传播,就好像给定的 @ExceptionHandler 方法一开始就没有匹配一样。

Spring MVC 对 @ExceptionHandler 方法的支持建立在 DispatcherServlet 级别,通过 HandlerExceptionResolver 机制实现。

媒体类型映射

除了异常类型外,@ExceptionHandler 方法还可以声明可生成的媒体类型。这允许根据 HTTP 客户端请求的媒体类型(通常在 "Accept" HTTP 请求头中)来细化错误响应。

应用程序可以直接在注解上声明可生成的媒体类型,针对相同的异常类型:

@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
}

@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
return "errorView";
}
java

在这里,方法处理相同的异常类型,但不会因为重复而被拒绝。相反,请求 "application/json" 的 API 客户端将收到 JSON 错误,而浏览器将获得 HTML 错误视图。每个 @ExceptionHandler 注解可以声明多种可生成的媒体类型,错误处理阶段的内容协商将决定使用哪种内容类型。

方法参数

@ExceptionHandler 方法支持以下参数:

方法参数描述
异常类型用于访问引发的异常。
HandlerMethod用于访问引发异常的控制器方法。
WebRequest, NativeWebRequest无需直接使用 Servlet API 即可通用访问请求参数以及请求和会话属性。
jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse选择任何特定的请求或响应类型(例如 ServletRequestHttpServletRequest 或 Spring 的 MultipartRequestMultipartHttpServletRequest)。
jakarta.servlet.http.HttpSession强制要求会话存在。因此,此类参数永远不会为 null
请注意,会话访问不是线程安全的。如果允许多个请求同时访问会话,请考虑将 RequestMappingHandlerAdapter 实例的 synchronizeOnSession 标志设置为 true
java.security.Principal当前认证的用户 — 如果已知,可能是特定的 Principal 实现类。
HttpMethod请求的 HTTP 方法。
java.util.Locale当前请求的区域设置,由最具体的 LocaleResolver 确定 — 实际上是配置的 LocaleResolverLocaleContextResolver
java.util.TimeZone, java.time.ZoneId与当前请求关联的时区,由 LocaleContextResolver 确定。
java.io.OutputStream, java.io.Writer用于访问 Servlet API 暴露的原始响应体。
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap用于访问错误响应的模型。始终为空。
RedirectAttributes指定在重定向情况下使用的属性 — (即附加到查询字符串)以及在重定向后请求之前临时存储的闪存属性。请参阅重定向属性闪存属性
@SessionAttribute用于访问任何会话属性,与由于类级 @SessionAttributes 声明而存储在会话中的模型属性形成对比。有关更多详细信息,请参阅 @SessionAttribute
@RequestAttribute用于访问请求属性。有关更多详细信息,请参阅 @RequestAttribute

返回值

@ExceptionHandler 方法支持以下返回值:

返回值描述
@ResponseBody返回值通过 HttpMessageConverter 实例进行转换并写入响应。参见 @ResponseBody
HttpEntity<B>, ResponseEntity<B>返回值指定完整响应(包括 HTTP 头和正文)通过 HttpMessageConverter 实例进行转换并写入响应。参见 ResponseEntity
ErrorResponse用于渲染带有正文详细信息的 RFC 9457 错误响应,参见 错误响应
ProblemDetail用于渲染带有正文详细信息的 RFC 9457 错误响应,参见 错误响应
String通过 ViewResolver 实现解析的视图名称,并与隐式模型一起使用——通过命令对象和 @ModelAttribute 方法确定。处理方法还可以通过声明 Model 参数(前面描述过)以编程方式丰富模型。
View用于渲染的 View 实例,并与隐式模型一起使用——通过命令对象和 @ModelAttribute 方法确定。处理方法还可以通过声明 Model 参数(前面描述过)以编程方式丰富模型。
java.util.Map, org.springframework.ui.Model通过 RequestToViewNameTranslator 隐式确定视图名称后,添加到隐式模型中的属性。
@ModelAttribute通过 RequestToViewNameTranslator 隐式确定视图名称后,添加到模型中的属性。

注意,@ModelAttribute 是可选的。参见本表末尾的“任何其他返回值”。
ModelAndView 对象使用的视图和模型属性,以及可选的响应状态。
void如果方法具有 void 返回类型(或 null 返回值),并且还具有 ServletResponseOutputStream 参数或 @ResponseStatus 注解,则认为该方法已完全处理了响应。如果控制器进行了积极的 ETaglastModified 时间戳检查(参见 控制器 了解详细信息),同样如此。

如果以上条件均不满足,void 返回类型还可以表示 REST 控制器的“无响应正文”或 HTML 控制器的默认视图名称选择。
任何其他返回值如果返回值未与上述任何情况匹配,并且不是简单类型(由 BeanUtils#isSimpleProperty 确定),默认情况下,它将被视为要添加到模型中的模型属性。如果是简单类型,则保持未解析状态。