Spring Boot - 全局异常处理
# 1. 全局异常处理的意义
在构建Web应用时,异常处理是一个非常重要的环节。当应用程序执行过程中出现异常情况时,如果不进行适当的处理,可能会导致以下问题:
- 向前端暴露敏感的错误信息,如堆栈跟踪
- 返回不友好的错误信息,影响用户体验
- 错误响应格式不统一,增加前端处理难度
- 大量的重复代码用于处理各种异常情况
在业务代码中,当遇到异常中断操作时,最直接的方式是返回一个简单的错误消息。但是,在复杂的代码调用链路中,这种方式会使代码变得臃肿且难以维护。一个更优雅的解决方案是抛出特定的异常,并通过全局异常处理机制统一捕获并处理这些异常,最后将格式化的响应返回给客户端。
Spring Boot提供了强大的全局异常处理机制,使开发者能够集中管理各种异常,并以一致的方式响应客户端请求。本文将详细介绍如何在Spring Boot应用中实现全局异常处理。
# 2. Spring Boot全局异常处理实现
Spring Boot通过以下关键组件实现全局异常处理:
- @ControllerAdvice/@RestControllerAdvice注解:标记一个类为全局异常处理器
- @ExceptionHandler注解:指定要处理的异常类型
- @ResponseStatus注解:定义返回的HTTP状态码
这些组件结合起来,可以创建一个强大的异常处理系统,捕获应用中抛出的任何类型的异常,并将其转换为结构化的API响应。
# 2.1 所需依赖
要实现全局异常处理,需要以下Maven依赖:
<!-- Spring Boot Web 依赖,提供Web应用所需基础功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 参数校验依赖,用于处理校验相关的异常 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 2.2 全局异常处理类实现
下面是一个综合的全局异常处理类示例,包含了对各种常见异常的处理:
/**
* 全局异常处理器
* 用于集中处理应用中抛出的各类异常,并将其转换为统一的API响应格式
*
* @RestControllerAdvice 注解结合了@ControllerAdvice和@ResponseBody,用于处理Controller层抛出的异常并自动将结果转为JSON
* @ResponseBody 将返回值转换为JSON格式响应
* @Slf4j Lombok提供的日志注解,自动创建日志对象
* @ConditionalOnWebApplication 只在Web应用环境下激活此组件
* @ConditionalOnClass 只有在指定类存在时才激活此组件
* @Order 设置处理器的优先级,HIGHEST_PRECEDENCE表示最高优先级
*/
@RestControllerAdvice
@ResponseBody
@Slf4j
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
/**
* 处理缺少请求参数异常
* 当请求缺少必须的参数时触发
*
* @param e 缺少请求参数异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<Object> handleError(MissingServletRequestParameterException e) {
// 记录警告级别日志
log.warn("缺少请求参数:{}", e.getMessage());
// 构造友好的错误信息
String message = String.format("缺少必要的请求参数: %s", e.getParameterName());
// 返回统一的错误响应
return HttpResult.error(ResponseStatusEnum.PARAM_MISS, message);
}
/**
* 处理请求参数类型不匹配异常
* 当请求参数的类型与方法参数类型不匹配时触发
*
* @param e 参数类型不匹配异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<Object> handleError(MethodArgumentTypeMismatchException e) {
// 记录警告级别日志
log.warn("请求参数格式错误:{}", e.getMessage());
// 构造友好的错误信息
String message = String.format("请求参数格式错误: %s", e.getName());
// 返回统一的错误响应
return HttpResult.fail(ResponseStatusEnum.PARAM_TYPE_ERROR, message);
}
/**
* 处理方法参数校验失败异常
* 当使用@Valid或@Validated注解对方法参数进行校验,校验失败时触发
*
* @param e 参数校验失败异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<Object> handleError(MethodArgumentNotValidException e) {
// 记录警告级别日志
log.warn("参数验证失败:{}", e.getMessage());
// 委托给通用的BindingResult处理方法
return handleError(e.getBindingResult());
}
/**
* 处理参数绑定异常
* 当Spring无法将请求参数绑定到方法参数时触发
*
* @param e 参数绑定异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<Object> handleError(BindException e) {
// 记录警告级别日志
log.warn("参数绑定失败:{}", e.getMessage());
// 委托给通用的BindingResult处理方法
return handleError(e.getBindingResult());
}
/**
* 处理BindingResult,提取错误信息
* 这是一个私有辅助方法,用于从BindingResult中提取错误信息
*
* @param result 包含绑定错误信息的对象
* @return 统一格式的错误响应
*/
private Response<Object> handleError(BindingResult result) {
// 获取第一个字段错误
FieldError error = result.getFieldError();
// 构造友好的错误信息:字段名 + 错误消息
String message = String.format("%s:%s", error.getField(), error.getDefaultMessage());
// 返回统一的错误响应
return HttpResult.fail(ResponseStatusEnum.PARAM_BIND_ERROR, message);
}
/**
* 处理约束违反异常
* 当通过@Validated对方法、类或参数上的约束检查失败时触发
*
* @param e 约束违反异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<Object> handleError(ConstraintViolationException e) {
// 记录警告级别日志
log.warn("参数验证失败:{}", e.getMessage());
// 获取所有违反的约束
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
// 获取第一个违反约束
ConstraintViolation<?> violation = violations.iterator().next();
// 获取属性路径的叶节点名称(属性名)
String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
// 构造友好的错误信息:属性名 + 错误消息
String message = String.format("%s:%s", path, violation.getMessage());
// 返回统一的错误响应
return HttpResult.fail(ResponseStatusEnum.PARAM_VALID_ERROR, message);
}
/**
* 处理资源未找到异常
* 当请求的URL没有对应的处理器时触发
*
* @param e 资源未找到异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Response<Object> handleError(NoHandlerFoundException e) {
// 记录错误级别日志
log.error("404 没找到请求:{}", e.getMessage());
// 返回统一的错误响应
return HttpResult.fail(ResponseStatusEnum.NOT_FOUND, e.getMessage());
}
/**
* 处理HTTP消息不可读异常
* 当请求体无法被解析为预期的对象时触发
*
* @param e HTTP消息不可读异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response<Object> handleError(HttpMessageNotReadableException e) {
// 记录错误级别日志
log.error("消息不能读取:{}", e.getMessage());
// 返回统一的错误响应
return HttpResult.fail(ResponseStatusEnum.MSG_NOT_READABLE, e.getMessage());
}
/**
* 处理HTTP请求方法不支持异常
* 当请求的HTTP方法不被支持时触发,如期望GET但使用了POST
*
* @param e HTTP请求方法不支持异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public Response<Object> handleError(HttpRequestMethodNotSupportedException e) {
// 记录错误级别日志
log.error("不支持当前请求方法:{}", e.getMessage());
// 返回统一的错误响应
return HttpResult.error(ResponseStatusEnum.METHOD_NOT_SUPPORTED, e.getMessage());
}
/**
* 处理HTTP媒体类型不支持异常
* 当请求的Content-Type不被支持时触发
*
* @param e HTTP媒体类型不支持异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public Response<Object> handleError(HttpMediaTypeNotSupportedException e) {
// 记录错误级别日志
log.error("不支持当前媒体类型:{}", e.getMessage());
// 返回统一的错误响应
return HttpResult.error(ResponseStatusEnum.MEDIA_TYPE_NOT_SUPPORTED, e.getMessage());
}
/**
* 处理HTTP媒体类型不可接受异常
* 当服务器无法生成客户端可接受的响应格式时触发
*
* @param e HTTP媒体类型不可接受异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public Response<Object> handleError(HttpMediaTypeNotAcceptableException e) {
// 记录错误级别日志
log.error("媒体类型异常:{}", e.getMessage());
// 返回统一的错误响应
return HttpResult.error(ResponseStatusEnum.MEDIA_TYPE_NOT_SUPPORTED, e.getMessage());
}
/**
* 处理所有未捕获的可抛出对象
* 捕获Throwable以确保任何类型的异常都能被处理
*
* @param e 可抛出对象
* @return 统一格式的错误响应
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Response<Object> handleError(Throwable e) {
// 记录错误级别日志,包括异常堆栈
log.error("服务器异常", e);
// 检查异常消息是否为空,为空则使用默认消息
return HttpResult.error(ResponseStatusEnum.INTERNAL_SERVER_ERROR,
(Objects.isNull(e.getMessage()) ? ResponseStatusEnum.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage()));
}
/**
* 处理一般Exception异常,作为兜底处理
* 这是最后的防线,确保所有异常都能被处理
*
* @param e 异常对象
* @return 统一格式的错误响应
*/
@ExceptionHandler(value = Exception.class)
public Response<Object> defaultExceptionHandler(Throwable e) {
// 记录错误级别日志,包括异常堆栈
log.error("服务器异常", e);
// 返回统一的错误响应,使用默认错误信息
return HttpResult.error(ResponseStatusEnum.INTERNAL_SERVER_ERROR);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# 3. 自定义业务异常处理
除了处理Spring框架内置的异常外,还可以创建和处理自定义业务异常,使异常处理更加灵活和符合业务需求。
# 3.1 创建自定义业务异常
/**
* 业务异常基类
* 所有自定义业务异常应继承此类
*/
public class BusinessException extends RuntimeException {
/**
* 错误码
*/
private Integer code;
/**
* 错误状态
*/
private String status;
/**
* 构造函数
*
* @param message 错误消息
*/
public BusinessException(String message) {
super(message);
}
/**
* 构造函数
*
* @param message 错误消息
* @param cause 异常原因
*/
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
/**
* 构造函数
*
* @param statusEnum 响应状态枚举
*/
public BusinessException(ResponseStatusEnum statusEnum) {
super(statusEnum.getMessage());
this.code = statusEnum.getCode();
this.status = statusEnum.getStatus();
}
/**
* 构造函数
*
* @param statusEnum 响应状态枚举
* @param message 自定义错误消息
*/
public BusinessException(ResponseStatusEnum statusEnum, String message) {
super(message);
this.code = statusEnum.getCode();
this.status = statusEnum.getStatus();
}
// Getter方法
public Integer getCode() {
return code;
}
public String getStatus() {
return status;
}
}
/**
* 数据不存在异常
* 当查询的数据不存在时抛出
*/
public class DataNotFoundException extends BusinessException {
public DataNotFoundException(String message) {
super(ResponseStatusEnum.NOT_FOUND, message);
}
}
/**
* 权限不足异常
* 当用户没有足够权限执行操作时抛出
*/
public class UnauthorizedException extends BusinessException {
public UnauthorizedException(String message) {
super(ResponseStatusEnum.UN_AUTHORIZED, message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# 3.2 在全局异常处理器中处理自定义异常
将以下代码添加到全局异常处理器类中:
/**
* 处理业务异常
*
* @param e 业务异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(BusinessException.class)
public Response<Object> handleBusinessException(BusinessException e) {
// 记录警告级别日志
log.warn("业务异常:{}", e.getMessage());
// 如果异常中有错误码和状态,则使用它们
if (e.getCode() != null && e.getStatus() != null) {
return HttpResult.response(null, e.getCode(), e.getStatus(), e.getMessage());
}
// 否则使用默认的失败状态
return HttpResult.failMessage(e.getMessage());
}
/**
* 处理数据不存在异常
*
* @param e 数据不存在异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(DataNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Response<Object> handleDataNotFoundException(DataNotFoundException e) {
// 记录警告级别日志
log.warn("数据不存在:{}", e.getMessage());
// 返回统一的错误响应
return HttpResult.fail(ResponseStatusEnum.NOT_FOUND, e.getMessage());
}
/**
* 处理权限不足异常
*
* @param e 权限不足异常
* @return 统一格式的错误响应
*/
@ExceptionHandler(UnauthorizedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Response<Object> handleUnauthorizedException(UnauthorizedException e) {
// 记录警告级别日志
log.warn("权限不足:{}", e.getMessage());
// 返回统一的错误响应
return HttpResult.fail(ResponseStatusEnum.UN_AUTHORIZED, e.getMessage());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 4. 配置和启用全局异常处理
# 4.1 在Spring Boot应用中启用异常处理
要确保正确处理NoHandlerFoundException
异常,需要在application.properties
或application.yml
中添加以下配置:
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
1
2
3
4
5
6
2
3
4
5
6
这些配置的作用是:
throw-exception-if-no-handler-found: true
- 当没有找到处理请求的控制器方法时抛出异常add-mappings: false
- 禁止Spring Boot自动添加静态资源处理器,确保所有未找到的资源请求都会抛出异常
# 4.2 使用自定义业务异常
在业务代码中使用自定义异常:
/**
* 用户服务示例
* 展示如何在业务代码中使用自定义异常
*/
@Service
public class UserService {
/**
* 根据ID获取用户
*
* @param userId 用户ID
* @return 用户对象
* @throws DataNotFoundException 当用户不存在时抛出
*/
public User getUserById(Long userId) {
User user = userRepository.findById(userId);
if (user == null) {
// 抛出自定义异常,将被全局异常处理器捕获
throw new DataNotFoundException("用户不存在:" + userId);
}
return user;
}
/**
* 删除用户
*
* @param userId 用户ID
* @param currentUser 当前操作的用户
* @throws UnauthorizedException 当用户没有权限执行删除操作时抛出
*/
public void deleteUser(Long userId, User currentUser) {
if (!currentUser.isAdmin()) {
// 抛出自定义异常,将被全局异常处理器捕获
throw new UnauthorizedException("只有管理员才能删除用户");
}
// 执行删除逻辑...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 5. 异常处理最佳实践
# 5.1 异常处理的原则
- 统一性 - 所有异常应以统一的格式响应客户端
- 明确性 - 错误消息应清晰明确,帮助客户端理解问题
- 安全性 - 不要在响应中暴露敏感信息或详细的堆栈跟踪
- 分层性 - 根据异常类型提供不同层次的处理
- 日志记录 - 确保所有异常都被适当记录,便于问题排查
# 5.2 处理未捕获异常
即使设计了全面的异常处理机制,仍可能有一些异常未被明确处理。为了确保所有异常都能得到处理,应该:
- 为
Throwable
类型提供兜底处理器 - 记录详细的异常日志,包括堆栈跟踪
- 返回通用的错误消息,避免暴露敏感信息
# 5.3 区分业务异常和系统异常
- 业务异常 - 通常是预期的异常,表示业务规则验证失败等情况
- 系统异常 - 通常是非预期的异常,表示系统错误、连接问题等
对于业务异常,可以返回具体的错误信息;对于系统异常,应返回通用的错误消息,并在服务器端记录详细信息。
# 6. 实际使用
# 6.1 控制器中使用全局异常处理
/**
* 用户控制器示例
* 展示如何在控制器中依赖全局异常处理
*/
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
/**
* 获取用户信息
* 不需要显式处理DataNotFoundException,由全局异常处理器负责
*/
@GetMapping("/{id}")
public Response<User> getUserById(@PathVariable Long id) {
// 如果用户不存在,userService会抛出DataNotFoundException
// 该异常会被全局异常处理器捕获并转换为适当的响应
User user = userService.getUserById(id);
return HttpResult.ok(user);
}
/**
* 创建用户
* 展示如何依赖参数验证和全局异常处理
*/
@PostMapping
public Response<User> createUser(@Valid @RequestBody UserCreateRequest request) {
// @Valid注解会触发请求验证,验证失败时会抛出MethodArgumentNotValidException
// 该异常会被全局异常处理器捕获并转换为适当的响应
User user = userService.createUser(request);
return HttpResult.ok(user);
}
/**
* 删除用户
* 不需要显式处理UnauthorizedException,由全局异常处理器负责
*/
@DeleteMapping("/{id}")
public Response<Void> deleteUser(@PathVariable Long id, @RequestAttribute User currentUser) {
// 如果当前用户没有权限,userService会抛出UnauthorizedException
// 该异常会被全局异常处理器捕获并转换为适当的响应
userService.deleteUser(id, currentUser);
return HttpResult.okMessage("用户删除成功");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 6.2 异常响应示例
成功响应:
{
"code": 200,
"status": "success",
"message": "操作成功",
"timestamp": 1688556681000,
"data": {
"id": 1,
"username": "john_doe",
"email": "john@example.com"
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
数据不存在异常响应:
{
"code": 404,
"status": "fail",
"message": "用户不存在:123",
"timestamp": 1688556682000,
"data": null
}
1
2
3
4
5
6
7
2
3
4
5
6
7
参数验证失败响应:
{
"code": 400,
"status": "fail",
"message": "username:用户名不能为空",
"timestamp": 1688556683000,
"data": null
}
1
2
3
4
5
6
7
2
3
4
5
6
7
权限不足异常响应:
{
"code": 401,
"status": "fail",
"message": "只有管理员才能删除用户",
"timestamp": 1688556684000,
"data": null
}
1
2
3
4
5
6
7
2
3
4
5
6
7
编辑此页 (opens new window)
上次更新: 2025/04/05, 20:16:54