SpringMVC 全局异常处理:`@ControllerAdvice` 与 `ExceptionHandler`

SpringMVC 全局异常处理:@ControllerAdviceExceptionHandler,一场优雅的救火表演

各位观众老爷,今天咱们聊聊SpringMVC里的一项重要技能——全局异常处理。想想看,你辛辛苦苦写的代码,好不容易上线了,结果用户一顿操作猛如虎,啪,页面崩了,报了个大大的500错误,这可咋整?用户体验直接降到冰点,老板的脸色比锅底还黑。

为了避免这种惨剧,我们需要一套完善的异常处理机制。SpringMVC为我们提供了强大的武器:@ControllerAdvice@ExceptionHandler。有了它们,我们就能优雅地接住各种异常,给用户一个友好的提示,而不是让他们看到一堆火星文般的错误信息。

这就像一个高级餐厅,厨房(Controller)里偶尔会发生点小意外,比如盐放多了,辣椒面洒了,但我们不能让顾客直接看到厨房的混乱,而是通过服务员(@ControllerAdvice)把问题悄悄解决掉,然后端上一道美味的补救菜品(自定义错误页面或JSON响应)。

接下来,让我们深入了解这两个核心概念,看看它们是如何配合完成这场精彩的“救火表演”。

1. @ControllerAdvice:全局异常处理的中枢调度

@ControllerAdvice,顾名思义,是一个“控制器增强器”。它允许我们将一些通用的逻辑,比如异常处理、数据绑定、模型属性设置等,应用到所有(或部分)Controller上。你可以把它想象成一个总指挥,负责统筹全局的异常处理策略。

1.1 @ControllerAdvice 的作用范围

@ControllerAdvice 可以通过多种方式来限制其作用范围,使其只对特定的Controller生效。

  • 默认情况: 如果没有指定任何属性,@ControllerAdvice 会对所有Controller生效。
  • basePackages 属性: 可以指定一个或多个包名,使其只对这些包下的Controller生效。
  • basePackageClasses 属性: 可以指定一个或多个Class对象,使其只对这些Class对象所在的包下的Controller生效。
  • assignableTypes 属性: 可以指定一个或多个Class对象,使其只对这些Class对象或其子类的Controller生效。
  • annotations 属性: 可以指定一个或多个注解,使其只对被这些注解标记的Controller生效。
// 只对 com.example.controller 包下的 Controller 生效
@ControllerAdvice(basePackages = "com.example.controller")
public class GlobalExceptionHandler {
    // ...
}

// 只对 UserController 和 AdminController 生效
@ControllerAdvice(assignableTypes = {UserController.class, AdminController.class})
public class GlobalExceptionHandler {
    // ...
}

// 只对被 @RestController 注解标记的 Controller 生效
@ControllerAdvice(annotations = RestController.class)
public class GlobalExceptionHandler {
    // ...
}

1.2 @ControllerAdvice 的常见用法

  • 全局异常处理: 这是 @ControllerAdvice 最常用的场景,我们可以在其中定义 @ExceptionHandler 方法,来处理不同类型的异常。
  • 全局数据绑定: 可以使用 @InitBinder 注解,对请求参数进行预处理,比如格式化日期、校验参数等。
  • 全局模型属性设置: 可以使用 @ModelAttribute 注解,在每个请求处理之前,向模型中添加一些通用的属性,比如用户信息、系统配置等。

2. @ExceptionHandler:异常处理的特种兵

@ExceptionHandler 是一个方法级别的注解,用于指定处理特定类型的异常。它就像一个训练有素的特种兵,专门负责解决某一类问题。

2.1 @ExceptionHandler 的基本用法

@ExceptionHandler 注解需要指定一个或多个 Exception 类型,表示该方法能够处理的异常类型。当Controller中抛出这些类型的异常时,就会自动调用该方法进行处理。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public String handleException(Exception e) {
        // 处理所有Exception类型的异常
        return "服务器发生未知错误:" + e.getMessage();
    }

    @ExceptionHandler(value = NullPointerException.class)
    @ResponseBody
    public String handleNullPointerException(NullPointerException e) {
        // 处理 NullPointerException 类型的异常
        return "空指针异常:" + e.getMessage();
    }
}

在上面的例子中,handleException 方法会处理所有 Exception 类型的异常,而 handleNullPointerException 方法只会处理 NullPointerException 类型的异常。

2.2 @ExceptionHandler 的返回值

@ExceptionHandler 方法可以有多种返回值类型,根据不同的场景选择合适的类型。

  • ModelAndView: 返回一个 ModelAndView 对象,可以指定视图名称和模型数据,用于跳转到自定义的错误页面。
  • String: 返回一个字符串,通常用于指定视图名称,SpringMVC 会根据视图解析器来解析该字符串。
  • ResponseEntity: 返回一个 ResponseEntity 对象,可以设置HTTP状态码、响应头和响应体,用于返回JSON数据或自定义的错误信息。
  • void: 如果返回 void,则表示直接向响应流中写入数据,通常用于返回JSON数据。

2.3 @ExceptionHandler 的参数

@ExceptionHandler 方法可以接收以下类型的参数:

  • Exception: 抛出的异常对象,可以获取异常信息。
  • HttpServletRequest: 当前的HttpServletRequest对象,可以获取请求信息。
  • HttpServletResponse: 当前的HttpServletResponse对象,可以设置响应信息。
  • HttpSession: 当前的HttpSession对象,可以获取会话信息。
  • WebRequest: Spring的WebRequest对象,可以获取请求和会话信息。
  • Locale: 当前的Locale对象,可以获取本地化信息。

3. 实战演练:打造一个完善的全局异常处理方案

光说不练假把式,接下来我们通过一个具体的例子,来演示如何使用 @ControllerAdvice@ExceptionHandler 构建一个完善的全局异常处理方案。

假设我们有一个电商网站,用户可以购买商品。在购买过程中,可能会发生以下几种异常:

  • 商品不存在异常(ProductNotFoundException): 用户访问了一个不存在的商品。
  • 库存不足异常(InsufficientStockException): 用户购买的商品数量超过了库存。
  • 订单创建失败异常(OrderCreationException): 创建订单时发生错误。
  • 其他未知异常(Exception): 其他未知的异常情况。

为了更好地处理这些异常,我们需要定义一个全局异常处理类 GlobalExceptionHandler,并使用 @ExceptionHandler 注解来处理不同类型的异常。

package com.example.exception;

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 org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GlobalExceptionHandler {

    // 处理商品不存在异常
    @ExceptionHandler(value = ProductNotFoundException.class)
    public ModelAndView handleProductNotFoundException(ProductNotFoundException e, HttpServletRequest request) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("errorMessage", e.getMessage());
        modelAndView.addObject("url", request.getRequestURL());
        modelAndView.setViewName("error/404"); // 跳转到自定义的404页面
        return modelAndView;
    }

    // 处理库存不足异常
    @ExceptionHandler(value = InsufficientStockException.class)
    @ResponseBody
    public ResponseEntity<ErrorInfo<String>> handleInsufficientStockException(InsufficientStockException e, HttpServletRequest request) {
        ErrorInfo<String> errorInfo = new ErrorInfo<>();
        errorInfo.setCode(HttpStatus.BAD_REQUEST.value()); // 设置HTTP状态码为400
        errorInfo.setMessage(e.getMessage());
        errorInfo.setUrl(request.getRequestURL().toString());
        errorInfo.setData("当前库存:" + e.getCurrentStock()); // 返回当前库存信息
        return new ResponseEntity<>(errorInfo, HttpStatus.BAD_REQUEST); // 返回JSON数据
    }

    // 处理订单创建失败异常
    @ExceptionHandler(value = OrderCreationException.class)
    public String handleOrderCreationException(OrderCreationException e, HttpServletRequest request) {
        request.setAttribute("errorMessage", e.getMessage());
        request.setAttribute("url", request.getRequestURL());
        return "error/500"; // 跳转到自定义的500页面
    }

    // 处理其他未知异常
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseEntity<ErrorInfo<String>> handleException(Exception e, HttpServletRequest request) {
        ErrorInfo<String> errorInfo = new ErrorInfo<>();
        errorInfo.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); // 设置HTTP状态码为500
        errorInfo.setMessage("服务器发生未知错误:" + e.getMessage());
        errorInfo.setUrl(request.getRequestURL().toString());
        return new ResponseEntity<>(errorInfo, HttpStatus.INTERNAL_SERVER_ERROR); // 返回JSON数据
    }
}

// 自定义错误信息类
class ErrorInfo<T> {
    private Integer code;
    private String message;
    private String url;
    private T data;

    // getter and setter methods
    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

// 自定义异常类
class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(String message) {
        super(message);
    }
}

class InsufficientStockException extends RuntimeException {
    private int currentStock;

    public InsufficientStockException(String message, int currentStock) {
        super(message);
        this.currentStock = currentStock;
    }

    public int getCurrentStock() {
        return currentStock;
    }
}

class OrderCreationException extends RuntimeException {
    public OrderCreationException(String message) {
        super(message);
    }
}

在上面的代码中,我们定义了四个 @ExceptionHandler 方法,分别处理不同类型的异常。

  • handleProductNotFoundException 方法处理 ProductNotFoundException 异常,返回一个 ModelAndView 对象,跳转到自定义的 404 页面,并显示错误信息。
  • handleInsufficientStockException 方法处理 InsufficientStockException 异常,返回一个 ResponseEntity 对象,包含错误码、错误信息、请求URL和当前库存信息。
  • handleOrderCreationException 方法处理 OrderCreationException 异常,将错误信息和请求URL添加到 HttpServletRequest 中,然后跳转到自定义的 500 页面。
  • handleException 方法处理其他未知的异常,返回一个 ResponseEntity 对象,包含错误码、错误信息和请求URL。

这样,当Controller中抛出这些异常时,就会自动调用相应的 @ExceptionHandler 方法进行处理,给用户一个友好的提示,而不是让他们看到一堆火星文般的错误信息。

3.1 Controller示例

为了测试我们的全局异常处理方案,我们可以创建一个简单的Controller,模拟抛出不同的异常。

package com.example.controller;

import com.example.exception.InsufficientStockException;
import com.example.exception.OrderCreationException;
import com.example.exception.ProductNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @GetMapping("/product/{id}")
    public String getProduct(@PathVariable("id") Long id) {
        if (id == 1) {
            return "Product 1";
        } else if (id == 2) {
            throw new InsufficientStockException("库存不足", 10);
        } else if (id == 3) {
            throw new OrderCreationException("订单创建失败");
        } else {
            throw new ProductNotFoundException("商品不存在");
        }
    }

    @GetMapping("/test")
    public String test() {
        throw new RuntimeException("测试未知异常");
    }
}

在上面的Controller中,我们定义了两个接口:

  • /product/{id}:根据商品ID返回商品信息,或者抛出不同的异常。
  • /test:抛出一个RuntimeException,用于测试未知异常的处理。

3.2 测试结果

  • 访问 /product/1:返回 "Product 1"。
  • 访问 /product/2:返回 JSON 格式的错误信息,HTTP 状态码为 400。
  • 访问 /product/3:跳转到自定义的 500 页面,并显示错误信息。
  • 访问 /product/4:跳转到自定义的 404 页面,并显示错误信息。
  • 访问 /test:返回 JSON 格式的错误信息,HTTP 状态码为 500。

4. 总结与注意事项

通过上面的例子,我们可以看到 @ControllerAdvice@ExceptionHandler 在全局异常处理中起着至关重要的作用。它们可以帮助我们优雅地处理各种异常,提升用户体验,保证系统的稳定性。

在使用 @ControllerAdvice@ExceptionHandler 时,需要注意以下几点:

  • 异常的匹配顺序: @ExceptionHandler 方法的匹配顺序是按照异常类型的继承关系来的,子类异常的处理方法会优先于父类异常的处理方法。
  • 返回值的选择: 根据不同的场景选择合适的返回值类型,比如 ModelAndView、String、ResponseEntity 或 void。
  • 异常信息的展示: 在处理异常时,应该尽量返回友好的错误信息,而不是直接将异常堆栈信息暴露给用户。
  • 日志记录: 在处理异常时,应该记录详细的日志信息,方便排查问题。
  • 统一的错误码: 建议定义一套统一的错误码规范,方便前端进行统一处理。

5. 高级用法:自定义异常解析器

除了使用 @ExceptionHandler 注解,SpringMVC还提供了更高级的异常处理机制——自定义异常解析器 HandlerExceptionResolver

HandlerExceptionResolver 接口允许我们自定义异常解析逻辑,可以根据异常类型、请求信息等,来决定如何处理异常。

public interface HandlerExceptionResolver {

    /**
     * Try to resolve the given exception that got thrown during handler execution.
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler the executed handler, or {@code null} if none chosen at the time of the exception
     * (for example, if multipart resolution failed)
     * @param ex the exception that got thrown during handler execution
     * @return a ModelAndView that resolves to an error view, or {@code null} if the exception is not handled
     */
    @Nullable
    ModelAndView resolveException(
            HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}

我们可以实现 HandlerExceptionResolver 接口,并将其注册到Spring容器中,SpringMVC就会自动调用该解析器来处理异常。

自定义异常解析器可以提供更灵活的异常处理方式,比如可以根据不同的环境(开发环境、生产环境)来返回不同的错误信息,或者可以根据用户的权限来决定是否显示详细的错误信息。

由于篇幅限制,这里就不展开讲解自定义异常解析器的具体实现方式了。感兴趣的读者可以自行查阅相关资料。

6. 总结

好了,各位观众老爷,今天的SpringMVC全局异常处理就聊到这里。希望通过这篇文章,大家能够掌握 @ControllerAdvice@ExceptionHandler 的基本用法,并能够运用到实际项目中,打造一个更加健壮和用户友好的Web应用。

记住,优雅的救火表演,才是程序员的最高境界!下课!

发表回复

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