SSM 统一异常处理机制:从 SpringMVC 到业务层的异常捕获与返回

SSM 统一异常处理机制:从 SpringMVC 到业务层的异常捕获与返回

各位看官,大家好!今天咱们来聊聊一个在 SSM 项目中至关重要,但又容易被忽视的话题:统一异常处理。想象一下,你的用户正在兴致勃勃地使用你的应用,突然,页面上蹦出一个丑陋的、难以理解的错误信息,或者更糟糕,直接白屏了。这感觉是不是像在约会时,对方突然打了个惊天动地的饱嗝一样尴尬?

所以,一个好的异常处理机制,就像一位优雅的绅士,能够妥善地处理各种突发情况,给用户一个友好的提示,而不是让他们丈二和尚摸不着头脑。

今天,我们就来深入探讨如何在 SSM(SpringMVC + Spring + MyBatis)项目中构建一个统一的、优雅的异常处理机制,让你的应用在面对错误时也能保持体面。

为什么要统一异常处理?

在没有统一异常处理的情况下,通常会有以下问题:

  • 代码冗余: 每个 Controller 方法都可能需要 try-catch 块来处理异常,导致代码重复。
  • 错误信息不一致: 不同地方的异常处理方式可能不同,导致用户看到的错误信息格式不一致,体验糟糕。
  • 难以维护: 如果需要修改错误处理逻辑,需要在多个地方进行修改,容易出错。
  • 安全性问题: 某些未处理的异常可能会暴露敏感信息,例如数据库连接字符串等。

统一异常处理就像一个消防队,负责处理整个应用中出现的各种“火情”,将错误信息统一格式化,并返回给用户,从而提高应用的健壮性和用户体验。

如何实现统一异常处理?

在 SSM 项目中,我们可以通过以下几种方式实现统一异常处理:

  1. SpringMVC 的 HandlerExceptionResolver
  2. @ControllerAdvice 和 @ExceptionHandler
  3. 自定义异常类

下面我们来逐一介绍这些方法,并结合代码示例进行讲解。

1. SpringMVC 的 HandlerExceptionResolver

HandlerExceptionResolver 是 SpringMVC 提供的一个接口,允许我们自定义异常解析器,将异常转换为 ModelAndView 对象,从而控制错误页面的展示。

实现步骤:

  1. 创建一个类实现 HandlerExceptionResolver 接口。
  2. 实现 resolveException 方法,在该方法中处理异常,并返回 ModelAndView 对象。
  3. 在 SpringMVC 的配置文件中注册该异常解析器。

代码示例:

import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class GlobalExceptionHandler implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelAndView mv = new ModelAndView();
        // 记录日志
        System.err.println("发生异常: " + ex.getMessage());
        // 根据不同的异常类型,返回不同的错误页面
        if (ex instanceof NullPointerException) {
            mv.setViewName("error/500");
            mv.addObject("errorMessage", "空指针异常,请联系管理员");
        } else if (ex instanceof IllegalArgumentException) {
            mv.setViewName("error/400");
            mv.addObject("errorMessage", "参数错误,请检查输入");
        } else {
            mv.setViewName("error/default");
            mv.addObject("errorMessage", "服务器发生未知错误,请稍后再试");
        }
        return mv;
    }
}

SpringMVC 配置文件 (applicationContext.xml):

<bean id="globalExceptionHandler" class="com.example.GlobalExceptionHandler"/>

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="handlerExceptionResolvers">
        <list>
            <ref bean="globalExceptionHandler"/>
        </list>
    </property>
</bean>

优点:

  • 可以自定义错误页面,方便用户体验。

缺点:

  • 需要配置 XML 文件,相对繁琐。
  • 无法直接返回 JSON 格式的错误信息。
  • 需要手动处理各种异常类型,代码可读性较差。

2. @ControllerAdvice 和 @ExceptionHandler

@ControllerAdvice@ExceptionHandler 是 Spring 3.2 引入的注解,提供了一种更简洁、更灵活的异常处理方式。

实现步骤:

  1. 创建一个类,使用 @ControllerAdvice 注解标记。
  2. 在该类中创建方法,使用 @ExceptionHandler 注解标记,并指定要处理的异常类型。
  3. @ExceptionHandler 注解标记的方法中处理异常,并返回合适的响应。

代码示例:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleNullPointerException(NullPointerException ex) {
        System.err.println("空指针异常: " + ex.getMessage());
        Map<String, Object> response = new HashMap<>();
        response.put("code", 500);
        response.put("message", "空指针异常,请联系管理员");
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(IllegalArgumentException ex) {
        System.err.println("参数错误: " + ex.getMessage());
        Map<String, Object> response = new HashMap<>();
        response.put("code", 400);
        response.put("message", "参数错误,请检查输入");
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleException(Exception ex) {
        System.err.println("未知异常: " + ex.getMessage());
        Map<String, Object> response = new HashMap<>();
        response.put("code", 500);
        response.put("message", "服务器发生未知错误,请稍后再试");
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

优点:

  • 代码简洁,易于阅读和维护。
  • 可以灵活地返回不同格式的响应,例如 JSON、XML 等。
  • 无需配置 XML 文件。

缺点:

  • 需要手动处理各种异常类型,仍然存在代码冗余。

3. 自定义异常类

为了更好地组织和管理异常,我们可以自定义异常类,将异常信息封装到自定义异常中。

实现步骤:

  1. 创建一个自定义异常类,继承自 RuntimeExceptionException
  2. 在自定义异常类中定义错误码和错误信息。
  3. 在业务层抛出自定义异常。
  4. @ControllerAdvice 中处理自定义异常。

代码示例:

自定义异常类:

public class BusinessException extends RuntimeException {

    private int code;
    private String message;

    public BusinessException(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

业务层代码:

public void doSomething(String input) {
    if (input == null || input.isEmpty()) {
        throw new BusinessException(1001, "输入不能为空");
    }
    // ... 其他业务逻辑
}

@ControllerAdvice 代码:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException ex) {
        System.err.println("业务异常: " + ex.getMessage());
        Map<String, Object> response = new HashMap<>();
        response.put("code", ex.getCode());
        response.put("message", ex.getMessage());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleException(Exception ex) {
        System.err.println("未知异常: " + ex.getMessage());
        Map<String, Object> response = new HashMap<>();
        response.put("code", 500);
        response.put("message", "服务器发生未知错误,请稍后再试");
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

优点:

  • 代码结构清晰,易于维护。
  • 可以自定义错误码和错误信息,方便前端进行处理。
  • 可以将异常处理逻辑与业务逻辑分离,提高代码的可读性。

缺点:

  • 需要定义更多的类。

异常处理的最佳实践

结合以上三种方式,我们可以总结出一些异常处理的最佳实践:

  1. 使用 @ControllerAdvice@ExceptionHandler 进行统一异常处理。
  2. 自定义异常类,封装错误码和错误信息。
  3. 在业务层抛出自定义异常。
  4. @ControllerAdvice 中处理自定义异常和通用异常。
  5. 记录异常日志,方便排查问题。
  6. 返回统一格式的响应,方便前端进行处理。
  7. 对用户友好的错误提示,避免暴露敏感信息。

完整的示例代码

下面是一个完整的示例代码,展示了如何使用 @ControllerAdvice@ExceptionHandler 和自定义异常类来实现统一异常处理。

1. 自定义异常类:

public class BusinessException extends RuntimeException {

    private int code;
    private String message;

    public BusinessException(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

2. Service 层:

import org.springframework.stereotype.Service;

@Service
public class MyService {

    public String getData(String id) {
        if (id == null || id.isEmpty()) {
            throw new BusinessException(1001, "ID 不能为空");
        }
        if (id.equals("123")) {
            return "Data for ID 123";
        } else {
            throw new BusinessException(1002, "找不到对应的数据");
        }
    }
}

3. Controller 层:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    @Autowired
    private MyService myService;

    @GetMapping("/data/{id}")
    public String getData(@PathVariable String id) {
        return myService.getData(id);
    }
}

4. GlobalExceptionHandler:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException ex) {
        System.err.println("业务异常: " + ex.getMessage());
        Map<String, Object> response = new HashMap<>();
        response.put("code", ex.getCode());
        response.put("message", ex.getMessage());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleException(Exception ex) {
        System.err.println("未知异常: " + ex.getMessage());
        Map<String, Object> response = new HashMap<>();
        response.put("code", 500);
        response.put("message", "服务器发生未知错误,请稍后再试");
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

5. 效果:

  • 如果访问 /data/ (没有 ID),会返回:
{
  "code": 1001,
  "message": "ID 不能为空"
}
  • 如果访问 /data/abc (ID 不存在),会返回:
{
  "code": 1002,
  "message": "找不到对应的数据"
}
  • 如果访问 /data/123 (ID 存在),会返回:
Data for ID 123

总结

统一异常处理是构建健壮、用户友好的应用的关键。通过使用 SpringMVC 的 HandlerExceptionResolver@ControllerAdvice@ExceptionHandler,以及自定义异常类,我们可以实现一个优雅、可维护的异常处理机制。

希望这篇文章能帮助你更好地理解和应用统一异常处理,让你的应用在面对错误时也能保持体面和优雅。

记住,一个好的异常处理机制,就像一位优秀的管家,能够妥善地处理各种突发情况,让你的用户感到安心和舒适。

好了,今天的分享就到这里,我们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注