前言
在实际开发中,异常处理是一个非常重要的环节。合理的异常处理机制不仅能提高系统的健壮性,还能大大提升用户体验。本文将详细介绍SpringBoot中全局异常处理的几种实现方式。
为什么需要全局异常处理?
如果没有统一的异常处理机制,当系统发生异常时,可能会导致以下问题
- 用户体验差:用户可能看到一些技术性的错误信息,如堆栈跟踪
- 安全隐患:暴露系统内部错误详情可能会被攻击者利用
- 维护困难:分散在各处的异常处理代码增加了维护难度
- 响应格式不一致:不同接口返回的错误格式不统一,增加前端处理难度
下面,来看几种在SpringBoot中实现全局异常处理的方式。
方式一:@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler
这是SpringBoot中最常用的全局异常处理方式。
首先,定义一个统一的返回结果类:
typescript
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
// getter和setter方法省略
}
然后,定义自定义异常:
scala
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(String message) {
super(message);
this.code = 500;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}
创建全局异常处理类:
kotlin
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleValidationException(ConstraintViolationException e) {
return Result.error(400, "参数校验失败:" + e.getMessage());
}
/**
* 处理资源找不到异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Result<Void> handleNotFoundException(NoHandlerFoundException e) {
return Result.error(404, "请求的资源不存在");
}
/**
* 处理其他所有未捕获的异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
return Result.error(500, "服务器内部错误:" + e.getMessage());
}
}
优点
- 代码简洁清晰,易于维护
- 可以针对不同的异常类型定义不同的处理方法
- 与Spring MVC结合紧密,可以获取请求和响应上下文
方式二:实现HandlerExceptionResolver接口
HandlerExceptionResolver是Spring MVC中用于解析异常的接口,我们可以通过实现此接口来自定义异常处理逻辑。
java
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
Result<?> result;
// 处理不同类型的异常
if (ex instanceof BusinessException) {
BusinessException businessException = (BusinessException) ex;
result = Result.error(businessException.getCode(), businessException.getMessage());
} else {
result = Result.error(500, "服务器内部错误:" + ex.getMessage());
}
// 设置响应类型和状态码
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
try {
// 将错误信息写入响应
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(result));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
// 返回空的ModelAndView表示异常已经处理完成
return new ModelAndView();
}
}
优点
- 可以完全控制异常处理的过程
- 可以直接操作HttpServletRequest和HttpServletResponse
- 适合需要特殊处理的场景
缺点
- 代码相对复杂
- 无法利用Spring MVC的注解优势
方式三:使用SimpleMappingExceptionResolver
SimpleMappingExceptionResolver是HandlerExceptionResolver的一个简单实现,适用于返回错误视图的场景。
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import java.util.Properties;
@Configuration
public class ExceptionConfig {
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
// 设置默认错误页面
resolver.setDefaultErrorView("error/default");
// 设置异常映射
Properties mappings = new Properties();
mappings.setProperty(BusinessException.class.getName(), "error/business");
mappings.setProperty(RuntimeException.class.getName(), "error/runtime");
resolver.setExceptionMappings(mappings);
// 设置异常属性名,默认为"exception"
resolver.setExceptionAttribute("ex");
return resolver;
}
}
对应的错误页面模板(使用Thymeleaf):
xml
<!-- templates/error/business.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>业务错误</title>
</head>
<body>
<h1>业务错误</h1>
<p th:text="${ex.message}">错误信息</p>
</body>
</html>
优点
- 配置简单
- 适合返回错误页面的场景
缺点
- 主要适用于返回视图,不适合RESTful API
- 灵活性有限
方式四:自定义ErrorController
Spring Boot提供了BasicErrorController来处理应用中的错误,我们可以通过继承或替换它来自定义错误处理逻辑。
java
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController implements ErrorController {
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<Result<Void>> handleError(HttpServletRequest request) {
HttpStatus status = getStatus(request);
Integer statusCode = status.value();
String message = "未知错误";
switch (statusCode) {
case 404:
message = "请求的资源不存在";
break;
case 403:
message = "没有权限访问该资源";
break;
case 500:
message = "服务器内部错误";
break;
default:
break;
}
return new ResponseEntity<>(Result.error(statusCode, message), status);
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public String handleErrorHtml(HttpServletRequest request, Map<String, Object> model) {
HttpStatus status = getStatus(request);
// 添加错误信息到模型
model.put("status", status.value());
model.put("message", status.getReasonPhrase());
// 返回错误页面
return "error/error";
}
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
} catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}
优点
- 可以同时处理HTML和JSON响应
- 对所有未处理的错误提供统一的处理方式
- 可以获取错误的状态码和详细信息
缺点
- 只能处理已经发生的HTTP错误,无法拦截自定义异常
- 一般作为兜底方案使用
方式五:使用错误页面模板
Spring Boot支持通过静态HTML页面或模板来展示特定状态码的错误。只需要在templates/error/目录下创建对应状态码的页面即可。
目录结构:
css
src/
main/
resources/
templates/
error/
404.html
500.html
error.html # 默认错误页面
例如,404.html的内容:
xml
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>页面不存在</title>
</head>
<body>
<h1>404 - 页面不存在</h1>
<p>您请求的页面不存在,请检查URL是否正确。</p>
<a href="/">返回首页</a>
</body>
</html>
优点
- 配置极其简单,只需创建对应的页面即可
- 适合简单的Web应用
缺点
- 灵活性有限
- 不适合RESTful API
- 无法处理自定义异常
实战示例:完整的异常处理体系
下面提供一个完整的异常处理体系示例,组合了多种方式:
- 首先,创建异常体系:
scala
// 基础异常类
public abstract class BaseException extends RuntimeException {
private final int code;
public BaseException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
// 业务异常
public class BusinessException extends BaseException {
public BusinessException(String message) {
super(400, message);
}
public BusinessException(int code, String message) {
super(code, message);
}
}
// 系统异常
public class SystemException extends BaseException {
public SystemException(String message) {
super(500, message);
}
public SystemException(int code, String message) {
super(code, message);
}
}
// 权限异常
public class PermissionException extends BaseException {
public PermissionException(String message) {
super(403, message);
}
}
- 创建全局异常处理器:
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理自定义基础异常
*/
@ExceptionHandler(BaseException.class)
public Result<?> handleBaseException(BaseException e) {
logger.error("业务异常:{}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常(@Valid)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
String errorMsg = fieldErrors.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
logger.error("参数校验错误:{}", errorMsg);
return Result.error(400, "参数校验错误: " + errorMsg);
}
/**
* 处理所有其他异常
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleException(Exception e) {
logger.error("系统异常:", e);
return Result.error(500, "服务器内部错误,请联系管理员");
}
}
- 使用示例:
less
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/{id}")
public Result<User> getUser(@PathVariable Long id) {
if (id <= 0) {
throw new BusinessException("用户ID必须大于0");
}
if (id > 100) {
throw new SystemException("系统维护中");
}
if (id == 10) {
throw new PermissionException("没有权限查看此用户");
}
// 模拟查询用户
User user = new User(id, "用户" + id, "user" + id + "@example.com");
return Result.success(user);
}
}
各方式对比与使用建议
实现方式 | 适用场景 | 灵活性 | 复杂度 |
---|---|---|---|
@ControllerAdvice + @ExceptionHandler | RESTful API、前后端分离项目 | 高 | 低 |
HandlerExceptionResolver | 需要精细控制异常处理过程的场景 | 高 | 中 |
SimpleMappingExceptionResolver | 传统Web应用,需要返回错误页面 | 中 | 低 |
自定义ErrorController | 需要自定义错误页面和错误响应的场景 | 中 | 中 |
错误页面模板 | 简单的Web应用,只需自定义错误页面 | 低 | 低 |
建议
-
对于RESTful API或前后端分离项目:优先选择@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler方式,它提供了良好的灵活性和简洁的代码结构。
-
对于需要返回错误页面的传统Web应用:可以使用SimpleMappingExceptionResolver或错误页面模板方式。
-
对于复杂系统:可以组合使用多种方式,例如
- 使用@ControllerAdvice处理业务异常
- 使用自定义ErrorController处理未捕获的HTTP错误
- 使用错误页面模板提供友好的错误页面
最佳实践总结
-
分层异常处理:根据业务需求,设计合理的异常继承体系,便于分类处理。
-
统一返回格式:无论成功还是失败,都使用统一的返回格式,便于前端处理。
-
合理记录日志:在异常处理中记录日志,可以帮助排查问题。不同级别的异常使用不同级别的日志。
-
区分开发和生产环境:在开发环境可以返回详细的错误信息,而在生产环境则应该隐藏敏感信息。
-
异常分类
- 业务异常:用户操作引起的可预期异常
- 系统异常:系统内部错误
- 第三方服务异常:调用外部服务失败等
-
不要忽略异常:即使是捕获异常后不需要处理,也应该至少记录日志。
结语
在Spring Boot应用中,全局异常处理是提高系统健壮性和用户体验的重要环节。通过本文介绍的几种实现方式,开发者可以根据实际需求选择合适的实现方案。在实际项目中,往往需要结合多种方式,构建一个完整的异常处理体系。