SpringBoot全局异常处理的5种实现方式

前言

在实际开发中,异常处理是一个非常重要的环节。合理的异常处理机制不仅能提高系统的健壮性,还能大大提升用户体验。本文将详细介绍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
  • 无法处理自定义异常

实战示例:完整的异常处理体系

下面提供一个完整的异常处理体系示例,组合了多种方式:

  1. 首先,创建异常体系:
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);
    }
}
  1. 创建全局异常处理器:
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, "服务器内部错误,请联系管理员");
    }
}
  1. 使用示例:
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应用,只需自定义错误页面

建议

  1. 对于RESTful API或前后端分离项目:优先选择@ControllerAdvice/@RestControllerAdvice + @ExceptionHandler方式,它提供了良好的灵活性和简洁的代码结构。

  2. 对于需要返回错误页面的传统Web应用:可以使用SimpleMappingExceptionResolver或错误页面模板方式。

  3. 对于复杂系统:可以组合使用多种方式,例如

    • 使用@ControllerAdvice处理业务异常
    • 使用自定义ErrorController处理未捕获的HTTP错误
    • 使用错误页面模板提供友好的错误页面

最佳实践总结

  1. 分层异常处理:根据业务需求,设计合理的异常继承体系,便于分类处理。

  2. 统一返回格式:无论成功还是失败,都使用统一的返回格式,便于前端处理。

  3. 合理记录日志:在异常处理中记录日志,可以帮助排查问题。不同级别的异常使用不同级别的日志。

  4. 区分开发和生产环境:在开发环境可以返回详细的错误信息,而在生产环境则应该隐藏敏感信息。

  5. 异常分类

    • 业务异常:用户操作引起的可预期异常
    • 系统异常:系统内部错误
    • 第三方服务异常:调用外部服务失败等
  6. 不要忽略异常:即使是捕获异常后不需要处理,也应该至少记录日志。

结语

在Spring Boot应用中,全局异常处理是提高系统健壮性和用户体验的重要环节。通过本文介绍的几种实现方式,开发者可以根据实际需求选择合适的实现方案。在实际项目中,往往需要结合多种方式,构建一个完整的异常处理体系。

相关推荐
huan9913 分钟前
Obsidian 插件篇 - 插件汇总简介
后端
周Echo周16 分钟前
5、vim编辑和shell编程【超详细】
java·linux·c++·后端·编辑器·vim
用户945085191249221 分钟前
一文搞懂过滤器和拦截器
java
AronTing21 分钟前
03-深入解析 Spring AOP 原理及源码
后端
逻辑重构鬼才23 分钟前
AES+RSA实现前后端加密通信:全方位安全解决方案
后端
Java水解26 分钟前
Java面试必问到的10道面试题
java·面试
卤蛋七号29 分钟前
JavaSE高级(一)
后端
martian66537 分钟前
《Spring Boot全栈开发指南:从入门到生产实践》
java·开发语言·spring boot
快来卷java1 小时前
深入剖析雪花算法:分布式ID生成的核心方案
java·数据库·redis·分布式·算法·缓存·dreamweaver
Java中文社群1 小时前
SpringAI用嵌入模型操作向量数据库!
后端·aigc·openai