SpringMVC 全局异常处理:@ControllerAdvice
与 ExceptionHandler
,一场优雅的救火表演
各位观众老爷,今天咱们聊聊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应用。
记住,优雅的救火表演,才是程序员的最高境界!下课!