错误处理 BasicErrorController
概述
Spring Boot 的错误处理机制通过 ErrorMvcAutoConfiguration 自动配置,核心是 BasicErrorController,它统一处理所有未被 @ExceptionHandler 捕获的异常,并根据请求的 Accept 头返回 HTML 或 JSON 格式的错误响应。
本文将深入拆解 BasicErrorController 的完整链路,涵盖自动配置条件、错误页面注册、内容协商、错误属性、模板解析、默认白标页面等 10 个细节点。
本文基于 Spring Boot 3.x 源码分析。
1. ErrorMvcAutoConfiguration 的触发条件
1.1 源码
java
// ErrorMvcAutoConfiguration.java
@AutoConfiguration(
before = WebMvcAutoConfiguration.class, // 在 WebMVC 自动配置之前
after = DispatcherServletAutoConfiguration.class) // 在 DispatcherServlet 之后
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@EnableConfigurationProperties({ ErrorProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
// ...
}1.2 三个条件的含义
java
// 条件 1: @ConditionalOnWebApplication(type = Type.SERVLET)
// 只对 Servlet Web 应用生效,Reactive Web 应用不触发
//
// 条件 2: @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// 需要 Servlet API 和 DispatcherServlet 都在 classpath
// - Servlet.class → javax.servlet-api / jakarta.servlet-api
// - DispatcherServlet.class → spring-webmvc
//
// 条件 3: @AutoConfiguration(after = DispatcherServletAutoConfiguration.class)
// 必须在 DispatcherServletAutoConfiguration 之后执行
// 确保 DispatcherServlet 已注册到容器1.3 自动配置的执行顺序
DispatcherServletAutoConfiguration ← 第 1 步
│ 创建 DispatcherServletRegistrationBean
│ 注册 DispatcherServlet 到 Servlet 容器
│
ErrorMvcAutoConfiguration ← 第 2 步
│ 创建 BasicErrorController
│ 创建 ErrorPageCustomizer
│ 注册 ErrorPage("/error")
│
WebMvcAutoConfiguration ← 第 3 步
│ 配置 WebMVC 组件
│ 注册自定义 HandlerMapping2. ErrorPageCustomizer 注册 /error 路径
2.1 源码
java
// ErrorMvcAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
static class ErrorPageCustomizerConfiguration {
@Bean
ErrorPageCustomizer errorPageCustomizer(
ServerProperties serverProperties,
DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(serverProperties, dispatcherServletPath);
}
}
// ErrorPageCustomizer.java —— 将错误路径注册到嵌入式容器
class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties serverProperties;
private final DispatcherServletPath dispatcherServletPath;
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
// 获取错误路径(默认 /error)
String errorPath = getErrorPath();
// 创建 ErrorPage(封装路径信息)
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(errorPath));
// 注册到 ErrorPageRegistry(通常是 Tomcat 的 StandardContext)
errorPageRegistry.addErrorPages(errorPage);
}
// 获取错误路径
private String getErrorPath() {
// 优先级:
// 1. server.error.path(配置项)
// 2. error.path(配置项,已废弃)
// 3. /error(默认值)
return this.serverProperties.getError().getPath();
}
}2.2 ErrorPage 在 Tomcat 中的注册
java
// TomcatServletWebServerFactory.java 中的处理
// ErrorPageRegistry 在 Tomcat 中的实现是 StandardContext
// StandardContext.addErrorPage(ErrorPage) 的调用链:
// 1. ErrorPageCustomizer.registerErrorPages()
// → errorPageRegistry.addErrorPages(new ErrorPage("/error"))
//
// 2. Tomcat 将 ErrorPage 存储在 StandardContext 的 errorPageSupport 中
//
// 3. 当 Tomcat 遇到未处理的异常或 HTTP 错误码时:
// 3.1 查找匹配的 ErrorPage
// 3.2 将请求转发到 /error
// 3.3 DispatcherServlet 处理 /error 请求
// 3.4 BasicErrorController 处理响应
// 错误码映射:
// ErrorPage("/error") — 无特定错误码,捕获所有 4xx/5xx
// ErrorPage(HttpStatus.NOT_FOUND, "/error/404.html") — 特定错误码
// ErrorPage(Throwable.class, "/error/500.html") — 特定异常2.3 错误转发流程
客户端请求: GET /api/users/999
│
├─ DispatcherServlet 处理
│ └─ HandlerMapping → HandlerAdapter
│ └─ 抛出异常 (如 NoHandlerFoundException 404)
│
├─ HandlerExceptionResolver 链
│ └─ 未处理 → Servlet 容器收到异常
│
├─ Tomcat StandardContext 匹配 ErrorPage("/error")
│ └─ RequestDispatcher.forward("/error")
│
├─ DispatcherServlet 再次处理 /error
│ └─ BasicErrorController 匹配 @RequestMapping("${server.error.path:/error}")
│
└─ BasicErrorController 返回响应
├─ 根据 Accept 头:
│ ├─ text/html → errorHtml() → HTML 模板
│ └─ application/json → error() → JSON
└─ 状态码: 原异常的状态码 (如 404, 500)3. BasicErrorController 的 @RequestMapping 路径
3.1 源码
java
// BasicErrorController.java
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController implements ErrorController {
// 路径 SpEL 解析:
// 1. 先尝试 server.error.path
// 2. 如果没有,再尝试 error.path(已废弃)
// 3. 如果都没有,使用 /error
//
// 所以相当于:
// if (server.error.path != null) → server.error.path
// else if (error.path != null) → error.path
// else → /error
}3.2 SpEL 解析的完整逻辑
java
// Spring Boot 的 PropertyResolver 会解析 @RequestMapping 中的 SpEL
// "${server.error.path:${error.path:/error}}"
// 解析过程:
// 1. 解析外层: ${server.error.path:默认值}
// → 查找 server.error.path 属性
// → 如果存在 → 使用其值
// → 如果不存在 → 使用默认值
//
// 2. 解析内层: ${error.path:/error}(作为默认值)
// → 查找 error.path 属性
// → 如果存在 → 使用其值
// → 如果不存在 → 使用 /error
// 配置示例:
server:
error:
path: /custom-error # → BasicErrorController 映射到 /custom-error
# 如果不设置 → 默认 /error3.3 Spring Boot 2.x vs 3.x 的对比
java
// Spring Boot 2.x
@RequestMapping("${server.error.path:${error.path:/error}}")
// 支持 server.error.path 和 error.path 两个属性
// Spring Boot 3.x
@RequestMapping("${server.error.path:${error.path:/error}}")
// 同上,但 error.path 已标记为废弃
// 建议统一使用 server.error.path4. errorHtml() 与 error() 的 ContentNegotiation 选择
4.1 源码
java
// BasicErrorController.java
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController implements ErrorController {
// 返回 HTML 格式的错误页面
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
// 1. 获取 HttpStatus
HttpStatus status = getStatus(request);
// 2. 获取错误属性(用于模板渲染)
Map<String, Object> model = getErrorAttributes(
request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
// 3. 选择错误视图(模板或默认白标页面)
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 4. 如果没有自定义模板 → 使用默认
return (modelAndView != null) ? modelAndView
: new ModelAndView("error", model);
}
// 返回 JSON 格式的错误信息
@RequestMapping
public ResponseEntity<Map<String, Object>> error(
HttpServletRequest request) {
// 1. 获取 HttpStatus
HttpStatus status = getStatus(request);
// 2. 获取错误属性
Map<String, Object> body = getErrorAttributes(
request, getErrorAttributeOptions(request, MediaType.ALL));
// 3. 返回 ResponseEntity
return new ResponseEntity<>(body, status);
}
}4.2 ContentNegotiation 的选择过程
客户端请求: GET /error
Accept: text/html
│
├─ errorHtml() 匹配 (produces = "text/html")
│ └─ 返回 ModelAndView → HTML
│
客户端请求: GET /error
Accept: application/json
│
├─ error() 匹配(无 produces 限制,兜底)
│ └─ 返回 ResponseEntity<Map> → JSON
│
客户端请求: GET /error
Accept: */*
│
├─ 优先匹配 errorHtml()(produces 更精确)
│ └─ 浏览器通常发送 Accept: text/html,*/* → 返回 HTML4.3 ErrorAttributeOptions 的控制
java
// 根据请求决定包含哪些错误属性
private ErrorAttributeOptions getErrorAttributeOptions(
HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
// 是否包含 exception 属性
if (this.errorProperties.isIncludeException()) {
options = options.including(Include.EXCEPTION);
}
// 是否包含 stacktrace 属性
if (isVisible(this.errorProperties.getIncludeStacktrace(), "stacktrace")) {
options = options.including(Include.STACKTRACE);
}
// 是否包含 message 属性
if (isVisible(this.errorProperties.getIncludeMessage(), "message")) {
options = options.including(Include.MESSAGE);
}
// 是否包含 binding-errors 属性
if (isVisible(this.errorProperties.getIncludeBindingErrors(), "binding-errors")) {
options = options.including(Include.BINDING_ERRORS);
}
return options;
}5. DefaultErrorAttributes.getErrorAttributes() 的 7 个属性
5.1 源码
java
// DefaultErrorAttributes.java
public class DefaultErrorAttributes implements ErrorAttributes, Ordered {
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
// 构建错误响应 Map
Map<String, Object> errorAttributes = new LinkedHashMap<>();
// 1. timestamp — 错误发生时间
errorAttributes.put("timestamp", ZonedDateTime.now());
// 2. status — HTTP 状态码
errorAttributes.put("status", getStatus(webRequest));
// 3. error — 状态码描述
// 如 404 → "Not Found", 500 → "Internal Server Error"
errorAttributes.put("error", getStatusReason(getStatus(webRequest)));
// 4. path — 请求路径
// 如 /api/users/999
errorAttributes.put("path", webRequest.getDescription(false));
// ===== 以下属性需要显式配置才能包含 =====
// 5. exception — 异常类型
// server.error.include-exception=true 或 ALWAYS
if (options.isIncluded(Include.EXCEPTION)) {
Throwable error = getError(webRequest);
if (error != null) {
errorAttributes.put("exception", error.getClass().getName());
}
}
// 6. message — 错误消息
// server.error.include-message=always
if (options.isIncluded(Include.MESSAGE)) {
Throwable error = getError(webRequest);
if (error != null) {
errorAttributes.put("message", error.getMessage());
}
}
// 7. trace — 堆栈跟踪
// server.error.include-stacktrace=always
if (options.isIncluded(Include.STACKTRACE)) {
Throwable error = getError(webRequest);
if (error != null) {
// 将堆栈跟踪转换为字符串
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
error.printStackTrace(pw);
errorAttributes.put("trace", sw.toString());
}
}
// 8. errors — 绑定错误详情(@Valid 校验失败时)
// server.error.include-binding-errors=always
if (options.isIncluded(Include.BINDING_ERRORS)) {
Throwable error = getError(webRequest);
if (error instanceof BindingResult) {
// 提取字段级错误
errorAttributes.put("errors",
((BindingResult) error).getAllErrors());
}
}
// 9. requestId — 请求 ID(可选)
// 如果有 RequestContextFilter 时
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
errorAttributes.put("requestId",
requestAttributes.getSessionId());
}
return errorAttributes;
}
}5.2 默认响应示例
json
// GET /api/users/999(用户不存在)
// 默认(仅包含 timestamp, status, error, path)
{
"timestamp": "2026-07-25T14:30:00.000+08:00",
"status": 404,
"error": "Not Found",
"path": "/api/users/999"
}
// 配置后(包含更多信息)
// server.error.include-message=always
// server.error.include-stacktrace=always
{
"timestamp": "2026-07-25T14:30:00.000+08:00",
"status": 404,
"error": "Not Found",
"message": "User with id 999 not found",
"path": "/api/users/999",
"trace": "com.example.UserNotFoundException: User with id 999 not found\n\tat ...",
"exception": "com.example.UserNotFoundException"
}
// @Valid 校验失败时(BindException)
{
"timestamp": "2026-07-25T14:30:00.000+08:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed for object 'user'",
"path": "/api/users",
"errors": [
{
"codes": ["NotBlank.user.name", "NotBlank.name", "NotBlank.java.lang.String", "NotBlank"],
"arguments": [{"codes": ["user.name", "name"], "defaultMessage": "name"}],
"defaultMessage": "姓名不能为空",
"objectName": "user",
"field": "name",
"rejectedValue": null
}
]
}5.3 各属性的配置控制
| 属性 | 配置项 | 可选值 | 默认 |
|---|---|---|---|
timestamp | 无(始终包含) | — | 始终 |
status | 无(始终包含) | — | 始终 |
error | 无(始终包含) | — | 始终 |
path | 无(始终包含) | — | 始终 |
exception | server.error.include-exception | true/false | false |
message | server.error.include-message | always/on-param/never | never |
trace | server.error.include-stacktrace | always/on-param/never | never |
errors | server.error.include-binding-errors | always/on-param/never | never |
requestId | 无 | — | 有条件包含 |
6. server.error.include-stacktrace=never 的条件渲染
6.1 源码
java
// ErrorProperties.java
@ConfigurationProperties(prefix = "server.error")
public class ErrorProperties {
/**
* 包含堆栈跟踪的策略。
* NEVER: 从不包含(安全)
* ALWAYS: 始终包含(生产环境不推荐)
* ON_PARAM: 根据请求参数 include-stacktrace 控制
*/
private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;
/**
* 包含异常类型。
*/
private boolean includeException = false;
/**
* 包含错误消息。
*/
private IncludeAttribute includeMessage = IncludeAttribute.NEVER;
/**
* 包含绑定校验错误。
*/
private IncludeAttribute includeBindingErrors = IncludeAttribute.NEVER;
// IncludeStacktrace 枚举
enum IncludeStacktrace {
NEVER, // 从不包含
ALWAYS, // 总是包含
ON_PARAM // 根据 ?include-stacktrace=true 参数控制
}
// IncludeAttribute 枚举
enum IncludeAttribute {
NEVER,
ALWAYS,
ON_PARAM
}
}6.2 条件判断逻辑
java
// BasicErrorController.java —— 判断是否包含 stacktrace
private boolean isVisible(
IncludeStacktrace includeStacktrace, String attributeName) {
if (includeStacktrace == IncludeStacktrace.ALWAYS) {
// 配置为 ALWAYS → 总是包含
return true;
}
if (includeStacktrace == IncludeStacktrace.ON_PARAM) {
// 配置为 ON_PARAM → 检查请求参数
// URL: /error?trace=true 或 /error?include-stacktrace=true
// 或 Header: X-Request-Error-Attributes: trace
return getRequestAttribute(attributeName);
}
// NEVER → 不包含堆栈跟踪
return false;
}
private boolean getRequestAttribute(String attributeName) {
// 从 RequestAttributes 中获取参数
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return false;
}
// 检查: ?trace=true 或 ?include-stacktrace=true
String value = requestAttributes.getRequestAttributes()
.getParameter(attributeName);
return "true".equals(value);
}6.3 配置示例
yaml
# 生产环境(安全模式,不暴露内部细节)
server:
error:
include-stacktrace: never # 不暴露堆栈
include-message: never # 不暴露错误消息
include-exception: false # 不暴露异常类型
include-binding-errors: never # 不暴露校验错误
# 开发环境(方便调试)
server:
error:
include-stacktrace: always
include-message: always
include-exception: true
include-binding-errors: always
# 按需暴露(?trace=true 时暴露)
server:
error:
include-stacktrace: on-param
include-message: on-param7. ErrorViewResolver 的选择逻辑
7.1 接口定义
java
// ErrorViewResolver.java
@FunctionalInterface
public interface ErrorViewResolver {
/**
* 解析错误视图。
*
* @param request 当前请求
* @param status HTTP 状态码
* @param model 错误属性模型
* @return 解析到的 ModelAndView,如果没有合适的视图则返回 null
*/
ModelAndView resolveErrorView(
HttpServletRequest request,
HttpStatus status,
Map<String, Object> model);
}7.2 DefaultErrorViewResolver 的实现
java
// DefaultErrorViewResolver.java
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
@Override
public ModelAndView resolveErrorView(
HttpServletRequest request,
HttpStatus status,
Map<String, Object> model) {
// 1. 按 HTTP 状态码精确匹配
// 如 404 → error/404.html
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView != null) {
return modelAndView;
}
// 2. 按状态码系列匹配
// 如 4xx → error/4xx.html
// 5xx → error/5xx.html
String series = status.series().name(); // "CLIENT_ERROR" 或 "SERVER_ERROR"
modelAndView = resolve(series, model);
if (modelAndView != null) {
return modelAndView;
}
// 3. 都没有 → 返回 null(使用默认白标页面)
return null;
}
// 实际解析
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 错误模板搜索路径:
// 1. classpath:/templates/error/{viewName}.html ← Thymeleaf 模板
// 2. classpath:/static/error/{viewName}.html ← 静态资源
// 3. classpath:/public/error/{viewName}.html ← 静态资源
// 4. classpath:/resources/error/{viewName}.html ← 静态资源
// 例如: resolve("404", model)
// → 搜索 classpath:/templates/error/404.html
// → 如果存在 → 返回 ModelAndView("error/404")
// → 如果不存在 → 继续搜索 classpath:/static/error/404.html
// → 如果不存在 → 返回 null
String errorViewName = "error/" + viewName;
View view = resolveView(errorViewName);
return (view != null) ? new ModelAndView(errorViewName, model) : null;
}
private View resolveView(String viewName) {
// 从 ResourceLoader 中查找资源
// 如果找到对应的视图模板 → 返回 View 对象
// 否则 → 返回 null
try {
Resource resource = this.applicationContext.getResource("classpath:/templates/" + viewName + ".html");
if (resource.exists()) {
return new InternalResourceView(viewName);
}
} catch (Exception ex) {
// 忽略
}
return null;
}
}7.3 模板解析优先级
BasicErrorController.errorHtml()
│
└─ resolveErrorView(request, response, status, model)
│
├─ 1. 遍历所有 ErrorViewResolver Bean
│ ├─ DefaultErrorViewResolver
│ │ ├─ 精确匹配: error/404.html
│ │ ├─ 系列匹配: error/4xx.html
│ │ └─ 无匹配 → null
│ │
│ └─ 自定义 ErrorViewResolver(可插拔)
│
├─ 2. 如果有自定义模板 → 返回 ModelAndView
│
└─ 3. 没有自定义模板 → 使用默认白标页面
└─ new ModelAndView("error", model)7.4 自定义错误页面示例
html
<!-- src/main/resources/templates/error/404.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>页面未找到</title>
</head>
<body>
<h1>404 - 页面未找到</h1>
<p th:text="${path}">请求路径</p>
<p th:text="${timestamp}">时间</p>
</body>
</html>
<!-- src/main/resources/templates/error/5xx.html(匹配所有 5xx 错误) -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>服务器错误</title>
</head>
<body>
<h1>500 - 服务器内部错误</h1>
<p>请稍后重试</p>
</body>
</html>
<!-- src/main/resources/templates/error/4xx.html(匹配所有 4xx 错误) -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>客户端错误</title>
</head>
<body>
<h1>请求错误</h1>
<p th:text="${status} + ' - ' + ${error}">错误信息</p>
</body>
</html>8. WhitelabelErrorViewConfiguration 的默认 HTML 页面
8.1 源码
java
// ErrorMvcAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(
prefix = "server.error.whitelabel",
name = "enabled",
matchIfMissing = true) // 默认启用
static class WhitelabelErrorViewConfiguration {
// 默认的错误视图名称
private static final String DEFAULT_ERROR_VIEW_NAME = "error";
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
View defaultErrorView() {
// 创建 SpelView
// SpelView 是一个特殊的 View 实现,使用 SpEL 表达式渲染
return new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>"
);
}
// 内部视图名称 Bean——避免触发 Thymeleaf 模板解析
@Bean
@ConditionalOnMissingBean
ErrorMvcAutoConfiguration.DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
}8.2 SpelView 的实现
java
// SpelView.java —— 使用 SpEL 表达式渲染简单的 HTML
class SpelView implements View {
private final String template; // 包含 ${...} 占位符的 HTML 模板
private final SpelExpressionParser parser = new SpelExpressionParser();
private final ParserContext parserContext = new ParserContext() {
@Override
public boolean isTemplate() { return true; }
@Override
public String getExpressionPrefix() { return "${"; }
@Override
public String getExpressionSuffix() { return "}"; }
};
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 1. 检查 Accept 头是否为 text/html
if (response.getContentType() == null) {
response.setContentType("text/html");
}
// 2. 创建 EvaluationContext(包含 model 中的所有属性)
BeanResolver beanResolver = new BeanResolver() {
@Override
public Object resolve(EvaluationContext context, String beanName) {
return model.get(beanName);
}
};
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setBeanResolver(beanResolver);
// 3. 解析模板中的 ${...} 占位符
// 例如: ${timestamp} → model.get("timestamp")
// ${status} → model.get("status")
// 如果 model 中没有 key → 返回 "null" 字符串
String rendered = parser.parseExpression(template, parserContext)
.getValue(ctx, String.class);
// 4. 写入响应
response.getWriter().append(rendered);
}
}8.3 默认白标页面的渲染结果
html
<!-- 默认渲染结果 -->
<html>
<body>
<h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>
<div id='created'>2026-07-25T14:30:00.000+08:00</div>
<div>There was an unexpected error (type=Not Found, status=404).</div>
<div>null</div>
</body>
</html>8.4 禁用默认白标页面
yaml
server:
error:
whitelabel:
enabled: false # 禁用默认白标页面
# 当没有自定义模板时,会返回空的响应体9. ErrorMvcAutoConfiguration.PresenceOfDispatcherServlet 内部条件
9.1 源码
java
// ErrorMvcAutoConfiguration.java
@AutoConfiguration(
before = WebMvcAutoConfiguration.class,
after = DispatcherServletAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
public class ErrorMvcAutoConfiguration {
// 内部条件: 确保 DispatcherServlet 已注册
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DispatcherServlet.class)
@ConditionalOnBean(DispatcherServlet.class)
static class PresenceOfDispatcherServlet {
// 这个内部类的存在仅仅是为了触发以下条件:
// 1. @ConditionalOnClass(DispatcherServlet.class)
// 确认 spring-webmvc 在 classpath
//
// 2. @ConditionalOnBean(DispatcherServlet.class)
// 确认 DispatcherServlet Bean 已注册到容器
// (由 DispatcherServletAutoConfiguration 创建)
//
// 如果 DispatcherServlet 未创建 → 整个 ErrorMvcAutoConfiguration 不生效
}
// 以下 Bean 依赖于 PresenceOfDispatcherServlet 的加载
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
@ConditionalOnMissingBean(value = BasicErrorController.class, search = SearchStrategy.CURRENT)
BasicErrorController basicErrorController(
ErrorAttributes errorAttributes,
ErrorProperties errorProperties,
List<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes,
errorProperties, errorViewResolvers);
}
// ...
}9.2 条件依赖链
DispatcherServletAutoConfiguration
│
├─ @Bean DispatcherServlet
│
└─ @Bean DispatcherServletRegistrationBean
ErrorMvcAutoConfiguration
│
├─ @ConditionalOnBean(DispatcherServlet.class) ← 等待 DispatcherServlet
│ └─ PresenceOfDispatcherServlet 内部类加载
│
├─ @Bean ErrorAttributes
├─ @Bean BasicErrorController
├─ @Bean ErrorPageCustomizer
└─ @Bean ErrorViewResolver9.3 为什么需要这个内部条件
java
// 原因: ErrorMvcAutoConfiguration 需要确保 DispatcherServlet 已经存在
// 如果 DispatcherServlet 不存在(如仅使用内嵌容器,不使用 Spring MVC)
// 则错误处理机制也不应该生效
// 对比:
// 没有 PresenceOfDispatcherServlet → @ConditionalOnClass 只检查 classpath
// 如果 classpath 有 spring-webmvc 但用户禁用了 DispatcherServlet
// → ErrorMvcAutoConfiguration 仍会加载
// → BasicErrorController 无法工作(没有 DispatcherServlet 处理请求)
//
// 有 PresenceOfDispatcherServlet → @ConditionalOnBean 检查容器
// 如果 DispatcherServlet Bean 不存在
// → ErrorMvcAutoConfiguration 不加载
// → 错误的处理交给 Servlet 容器的默认错误页面10. @ControllerAdvice + @ExceptionHandler 与 BasicErrorController 的优先级
10.1 优先级顺序
异常抛出
│
├─ 1. @ControllerAdvice + @ExceptionHandler(全局异常处理器)
│ ├─ 优先级最高
│ ├─ 可以完全控制响应格式和状态码
│ └─ 例如: 返回自定义 JSON 或错误页面
│
├─ 2. @ExceptionHandler in Controller(控制器内部的异常处理器)
│ ├─ 优先级次之
│ └─ 只对该控制器生效
│
├─ 3. HandlerExceptionResolver 链
│ ├─ DefaultHandlerExceptionResolver(Spring 默认)
│ │ └─ 处理 Spring MVC 标准异常(如 NoHandlerFoundException)
│ ├─ ResponseStatusExceptionResolver
│ │ └─ 处理 @ResponseStatus 标注的异常
│ └─ ExceptionHandlerExceptionResolver
│ └─ 处理 @ExceptionHandler 标注的方法
│
├─ 4. DispatcherServlet 捕获所有未处理异常
│ └─ sendError(statusCode) → Tomcat 检测到错误
│
└─ 5. Tomcat 转发到 ErrorPage("/error")
│
└─ 6. BasicErrorController 处理 /error 请求
├─ 生成 JSON 或 HTML 错误响应
└─ 如果还抛出异常 → 容器默认错误页面10.2 源码对比
java
// DispatcherServlet 中的异常处理链
// DispatcherServlet.java
protected ModelAndView processHandlerException(
HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 遍历 HandlerExceptionResolver 链
ModelAndView exMv = null;
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
// 尝试解析异常
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
// 解析成功 → 返回处理结果
return exMv;
}
}
// 所有 HandlerExceptionResolver 都无法处理 → 抛出异常
// DispatcherServlet 会调用 sendError()
throw ex;
}
// BasicErrorController 只有在前面的链都未处理时才生效
// 因为 BasicErrorController 处理的是 Tomcat 转发到 /error 的请求
// 而不是直接处理原始请求的异常10.3 使用 @ControllerAdvice 自定义错误
java
// 方式 1: @ControllerAdvice 全局异常处理(推荐)
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseBody
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
// 自定义错误响应格式
return new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"资源不存在",
ex.getMessage()
);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponse handleGeneral(Exception ex) {
// 兜底异常处理
return new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"服务器内部错误",
"请稍后重试"
);
}
// 自定义错误响应结构
record ErrorResponse(int status, String error, String message) {}
}
// 方式 2: 自定义 ErrorAttributes(影响 BasicErrorController 的输出)
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> attributes = super.getErrorAttributes(webRequest, options);
// 添加自定义属性
attributes.put("app_name", "My Application");
attributes.put("app_version", "1.0.0");
// 移除敏感属性
attributes.remove("trace"); // 确保不暴露堆栈
return attributes;
}
}10.4 两种方式的对比
| 特性 | @ControllerAdvice + @ExceptionHandler | BasicErrorController + ErrorAttributes |
|---|---|---|
| 控制粒度 | 精细(按异常类型分别处理) | 统一处理所有未捕获异常 |
| 响应格式 | 完全自定义(JSON/XML/HTML) | JSON(默认)或 HTML(模板) |
| 状态码 | 通过 @ResponseStatus 控制 | 自动从请求属性获取 |
| 模板支持 | 返回 ModelAndView 可配合模板 | 支持 error/{code}.html 模板 |
| 配置复杂度 | 需要手动创建类和方法 | 仅需配置 server.error.* |
| 适用场景 | 需要精细化异常处理 | 兜底错误处理 |
10.5 @ControllerAdvice 的优先级高于 BasicErrorController 的原因
java
// 原因:
// 1. @ControllerAdvice 中的 @ExceptionHandler 在 DispatcherServlet 中执行
// 此时请求还在原始路径(如 /api/users/999)
//
// 2. BasicErrorController 处理的是转发后的 /error 请求
// 只有原始请求的异常未被处理,Tomcat 才会转发到 /error
//
// 所以只要 @ControllerAdvice 覆盖了异常类型
// 就不会触发 BasicErrorController 的兜底处理
//
// 调用链对比:
//
// @ControllerAdvice 生效:
// /api/users/999 → DispatcherServlet → 抛出异常 → @ExceptionHandler → 返回自定义响应
// (不会触发 ErrorPage 转发)
//
// @ControllerAdvice 未覆盖:
// /api/users/999 → DispatcherServlet → 抛出异常 → 无 HandlerExceptionResolver 处理
// → sendError(404) → Tomcat 匹配 ErrorPage("/error")
// → 转发到 /error → BasicErrorController → 返回默认错误响应总结
| # | 细节点 | 核心要点 |
|---|---|---|
| ① | @ConditionalOnWebApplication(SERVLET) + @ConditionalOnClass | 只对 Servlet Web 应用生效,需要 Servlet API 和 DispatcherServlet |
| ② | ErrorPageCustomizer 注册 /error | ErrorPageRegistrar.addErrorPages(new ErrorPage("/error")) → Tomcat StandardContext 处理 |
| ③ | @RequestMapping 路径 | ${server.error.path:${error.path:/error}} SpEL 三级优先级解析 |
| ④ | errorHtml() 与 error() | produces = MediaType.TEXT_HTML_VALUE 匹配 HTML,兜底匹配 JSON |
| ⑤ | DefaultErrorAttributes 7 个属性 | timestamp/status/error/path 始终包含;exception/message/trace/errors 需配置 |
| ⑥ | include-stacktrace 条件渲染 | NEVER/ALWAYS/ON_PARAM 三种模式,ON_PARAM 通过 ?trace=true 控制 |
| ⑦ | ErrorViewResolver 选择 | 精确匹配 error/404.html → 系列匹配 error/4xx.html → 白标页面 |
| ⑧ | WhitelabelErrorViewConfiguration | SpelView 渲染 ${timestamp}、${status}、${error}、${message} 占位符 |
| ⑨ | PresenceOfDispatcherServlet | @ConditionalOnBean(DispatcherServlet.class) 内部条件,确保 DispatcherServlet 存在 |
| ⑩ | @ControllerAdvice 优先级 | @ExceptionHandler 在 DispatcherServlet 阶段处理,BasicErrorController 是兜底转发 |