前言

在项目开发的过程中, 不管是对底层数据库的操作过程, 还是业务层的处理过程, 还是控制层的处理过程, 都不可避免会遇到各种可预知的、 不可预知的异常需要处理。 如果对每个过程都单独作异常处理, 代码耦合度会比较高,开发工作量也会加大而且不好统一, 这也增加了代码的维护成本。

针对这种实际情况, 我们需要将所有类型的异常处理从各处理过程中解耦出来, 这样既保证了相关处理过程的功能单一, 也实现了异常信息的统一处理和维护。

同时, 我们也不希望直接把异常抛给用户, 应该对异常进行处理, 对错误信息进行封装, 然后返回一个友好的信息给用户。

自定义异常类相关知识回顾

img

异常的分类

  • Throwable 类是 Java 异常类型的顶层父类,一个对象只有是 Throwable类的实例,才是一个异常对象,才能被异常处理机制识别。
  • 按照错误严重性,从 Throwale 类中衍生出 Error 和 Exception 两大派系
    • Error(错误):程序在执行过程中所遇到的硬件或系统的错误。错误对程序而言是致命的,将导致程序无法运行。不允许捕获。当发生 Error 时,只能依靠外界干预。比如:内存溢出。

    • Exception(异常):是程序运行过程中,可以预料的意外情况。比如空指针,数组下标越界。异常出现可以被捕获处理掉,使程序继续运行。

  • Exception:又分为编译时异常和运行时异常。
    • 运行时异常都是 RuntimeException 类及其子类, 这些异常是不检查的异常, 是在程序运行的时候可能会发生的, 所以程序可以捕捉, 也可以不捕捉。程序应该从逻辑角度去尽量避免。如:空指针、数组下标越界等。
    • 编译时异常也叫检查异常,是运行时异常以外的异常, 也是 Exception 及其子类, 这些异常从程序的角度来说是必须经过捕捉检查处理的, 否则不能通过编译. 如 IOException、SQLException 等。
img

上面的异常体系结构图都是系统自带的,系统自己处理,但是很多时候项目会出现特有问题,而这些问题并未被 Java 所描述并封装成对象,所以对于这些特有的问题可以按照封装的思想,将特有的问题进行自定义异常封装。要想创建自定义异常,需要继承 Throwable 或者他的子类 Exception。

使用自定义异常类的步骤

img

错误处理机制的默认机制

默认机制流程

Spring Boot 对异常的处理有一套默认的机制

  • 当程序产生异常时,根据请求头中的 Content-Type 包含的内容来返回不同的响应信息。如果 Content-Type 是”text/html”,则以 HTML 格式返回,如果 Content-Type 是”application/json”,则以 JSON 格式返回。

错误处理的⾃动配置都在 ErrorMvcAutoConfiguration 中,两⼤核⼼机制:

  • SpringBoot 会自适应处理错误,响应页面或JSON数据

  • SpringMVC的错误处理机制依然保留,MVC处理不了,才会交给boot进行处理

如下,这张图将是整个默认处理机制的核心步骤总结

image-20250608153602955

如果你使用了以前 Spring MVC 错误处理的方式,那么就按照你的来,没有就按照默认的来

  • 这里就是使用的@ExceptionHandler,用于标记一个方法,该方法将处理特定类型的异常。默认情况下,它只处理 当前控制器类 中发生的异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RestController
    @RequestMapping("/api")
    public class UserController {

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
    // 可能抛出 UserNotFoundException
    return userService.getUserById(id);
    }

    // 处理当前控制器中的 UserNotFoundException
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND)
    .body("User not found: " + ex.getMessage());
    }
    }
  • @ControllerAdvice统一处理所有错误,通常用在全局错误处理器,一般是集中处理所有 Controller 发生的错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @ControllerAdvice
    public class GlobalExceptionHandler {

    // 处理所有控制器中的 NullPointerException
    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity<String> handleNullPointerException(NullPointerException ex) {
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
    .body("Internal server error: " + ex.getMessage());
    }

    // 处理所有控制器中的 MethodArgumentNotValidException(验证失败)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    ex.getBindingResult().getFieldErrors()
    .forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
    return ResponseEntity.badRequest().body(errors);
    }
    }

而在这里,可以实现可以通过自定义异常类和 @ResponseStatus 注解来简化异常处理

如果你使用自定义 Spring MVC 的错误,注意,Spring MVC 的异常处理遵循 就近原则

  1. 局部处理:优先使用控制器内部的 @ExceptionHandler
  2. 全局处理:如果控制器内部没有匹配的处理方法,则使用 @ControllerAdvice 中的全局处理方法。

这部分就是在这里提一下,下面在说Spring Boot 中的异常处理方案时候还是会继续说的。

所以Spring boot 在什么都不写的时候,会默认就有一个 /error目录。因为 Spring MVC 如果没有错误请求处理,就处理不了,就得转发到 Spring boot 处理错误的请求路径。

img

如果用 Postman 访问,则以 JSON 的格式返回异常信息

image-20250608154906069

发⽣错误以后,转发给/error路径,SpringBoot在底层写好⼀个 BasicErrorController的组件,专⻔ 处理这个请求

这个路径是可以设置的,配置文件中通过server.error.path=/error,表示错误发生后,错误请求将转发到这个路径进行处理

默认错误处理机制的实现:关键点在 BasicErrorController 这个类中

BasicErrorController 类

  • 这是 Spring Boot 默认的错误处理控制器,负责处理所有未被应用程序其他控制器处理的异常请求(默认映射到 /error 路径)
  • 它提供了两种错误响应方式:HTML 格式(errorHtml 方法)和 JSON 格式(error 方法)
  • 在处理 HTML 错误视图时,它依赖于 ErrorViewResolver 接口的实现来确定具体使用哪个视图模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 处理HTML格式的错误请求,返回错误页面视图
*
* @param request HTTP请求对象
* @param response HTTP响应对象
* @return 错误视图模型,如果未找到特定视图则返回默认"error"视图
*/
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
// 获取请求对应的HTTP状态码
HttpStatus status = this.getStatus(request);
// 获取错误属性并封装到不可修改的Map中
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
// 设置响应状态码
response.setStatus(status.value());
// 尝试解析错误视图
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
// 如果未找到特定视图则返回默认"error"视图
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 处理非HTML格式的错误请求,返回JSON格式错误响应
*
* @param request HTTP请求对象
* @return 包含错误信息的响应实体
*/
@RequestMapping // 返回ResponseEntity, JSON
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 获取请求对应的HTTP状态码
HttpStatus status = this.getStatus(request);
// 处理无内容状态码的特殊情况
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
// 获取错误属性并封装到响应体中
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}

所以说,错误页面和错误信息是这么解析到的

1
2
3
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
// 如果未找到特定视图则返回默认"error"视图
return modelAndView != null ? modelAndView : new ModelAndView("error", model);

容器中专门的有⼀个错误视图解析器,在public class ErrorMvcAutoConfiguration,就是去哪个错误页面是这个方法解析器来决定的,其中涉及到的关键类 DefaultErrorViewResolver,会在下面说

1
2
3
4
5
6
@Bean
@ConditionalOnBean({DispatcherServlet.class})
@ConditionalOnMissingBean({ErrorViewResolver.class})
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

看一下 BasicErrorController类的关系,BasicErrorController类本身就是一个控制器,这个类是默认处理/error 请求的。那么响应页面的时候是怎么找到页面的呢?这里有一个关键类 DefaultErrorViewResolver

image-20250608155228098

DefaultErrorViewResolver 类:

  • 这是 ErrorViewResolver 接口的默认实现类,负责根据 HTTP 状态码解析对应的错误视图
  • 它尝试从模板引擎(如 Thymeleaf、FreeMarker)或静态资源目录中查找匹配的错误页面
  • 如果找不到精确匹配的错误页面(如 error/404.html),则尝试使用系列错误页面(如 error/4xx.htmlerror/5xx.html

响应页面时如何找到默认处理 /error 请求的呢,可以发现,页面当应用程序抛出异常时,Spring Boot 的错误处理流程大致如下:

  • 异常捕获:DispatcherServlet 捕获到未处理的异常,将请求转发到 /error 路径

  • 请求处理BasicErrorController 接收到 /error 请求,并根据请求的 Accept 头决定返回 HTML 还是 JSON 格式的响应

  • 视图解析:在处理 HTML 响应时,BasicErrorController 调用 DefaultErrorViewResolverresolveErrorView 方法来确定使用哪个视图模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    // 尝试解析精确匹配的状态码视图(如 error/404)
    ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
    // 如果找不到精确匹配的视图,则尝试解析系列视图(如 error/4xx)
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
    }

视图解析的流程大概如下:

resolveErrorView中,调用了reslove去解析精确匹配的状态码,然后尝试构建视图,然后resolve方法中会调用resolveResource获取静态资源

1
2
3
4
5
6
7
8
9
10
11
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 构建错误视图名称,格式为 "error/{statusCode}" 或 "error/{series}"
String errorViewName = "error/" + viewName;

// 检查是否存在可用的模板引擎来处理该视图
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);

// 如果存在模板引擎支持,则使用模板引擎渲染视图
// 否则尝试从静态资源中查找对应的 HTML 文件
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 遍历所有静态资源位置
String[] locations = this.resources.getStaticLocations();
for (String location : locations) {
try {
// 尝试加载资源
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
} catch (Exception e) {
// 忽略异常,继续检查下一个位置
}
}
return null;
}

所以,我们可以得到详细的规则,如下:

  • 解析⼀个错误⻚

    • 如果发⽣了500、404、503、403 这些错误

      • 如果有模板引擎,默认在 classpath:/templates/error/ 精确码 .html

      • 如果没有模板引擎,在静态资源⽂件夹下找 精确码 .html

    • 如果匹配不到 精确码 .html 这些精确的错误⻚,就去找 5xx.html4xx.html模糊匹配

      • 如果有模板引擎,默认在 classpath:/templates/error/5xx.html

      • 如果没有模板引擎,在静态资源⽂件夹下找 5xx.html

    • 如果模板引擎路径 templates 下有 error.html ⻚⾯,就直接渲染

  • 如果你需要定制错误页面,这个类的步骤很重要

  • 因为,这种机制使得开发者可以通过在 templates/error/static/error/ 目录下放置相应的 HTML 文件来自定义错误页面,同时保持了 Spring Boot 错误处理的默认行为。

而容器中有⼀个默认的名为 error 的 view; 提供了默认⽩⻚功能

1
2
3
4
5
6
7
8
9
@Bean(
name = {"error"}
)
@ConditionalOnMissingBean(
name = {"error"}
)
public View defaultErrorView() {
return this.defaultErrorView;
}

封装了JSON格式的错误信息

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnMissingBean(
value = {ErrorAttributes.class},
search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}

所以如何自定义一个异常的界面,这很有说法了

Spring Boot 中的异常处理方案

Spring Boot中自定义异常

全局异常处理:通过实现HandlerExceptionResolver接口或扩展ResponseEntityExceptionHandler类,实现全局的异常处理。

实现HandlerExceptionResolver

Spring提供了HandlerExceptionResolver接口,用于定义全局的异常处理逻辑。可以通过实现该接口,来自定义异常处理策略。

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("message", ex.getMessage());
modelAndView.setViewName("error");
return modelAndView;
}
}

扩展ResponseEntityExceptionHandler

ResponseEntityExceptionHandler是Spring提供的一个基类,包含了一些常见异常的处理逻辑。可以通过扩展该类,来实现自定义的异常处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), "Validation Failed", ex.getBindingResult().toString());
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
}

常见异常的处理方法

处理资源未找到异常

资源未找到异常(ResourceNotFoundException)是指请求的资源不存在,通常在RESTful API中较为常见。可以通过自定义异常类和异常处理方法来处理这种异常。

1
2
3
4
5
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}

在全局异常处理类中处理该异常:

1
2
3
4
5
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
处理数据验证异常

数据验证异常(ValidationException)通常在对输入数据进行验证时出现。可以通过在Controller中使用@Valid注解,对输入数据进行验证,并在全局异常处理类中处理验证异常。

1
2
3
4
5
6
7
8
9
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class User {
@NotNull
private Long id;
@Size(min = 2, message = "Name should have at least 2 characters")
private String name;
// Getters and Setters
}

在Controller中使用@Valid注解:

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
// Save user to database
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
}

在全局异常处理类中处理验证异常

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, WebRequest request) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
处理数据库异常

数据库异常(DatabaseException)通常在与数据库交互时出现。例如,违反唯一性约束、外键约束等。可以通过自定义异常类和异常处理方法来处理这种异常。

1
2
3
4
5
public class DatabaseException extends RuntimeException {
public DatabaseException(String message) {
super(message);
}
}

在全局异常处理类中处理数据库异常:

1
2
3
4
5
@ExceptionHandler(DatabaseException.class)
public ResponseEntity<?> handleDatabaseException(DatabaseException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}

自定义json响应

简介

在现代前后端分离的架构中,RESTful API 通常需要返回结构化的 JSON 错误响应,而不是 HTML 页面。Spring Boot 提供了多种方式来实现自定义 JSON 错误响应,使前端能够以统一的方式处理错误。

Spring 提供了一个非常方便的异常处理方案:控制器通知 (@ControllerAdvice@RestControllerAdvice),它将所有控制器作为一个 切面,利用切面技术来实现异常的通知。处理Controller层的异常处理就是通过@ControllerAdvice@ExceptionHandler注解,集中处理Controller层的异常。这两个注解的使用我在上面已经说过了,在这里就是扩展讲一下了

相关注解

@ExceptionHandler

@ExceptionHandler,用于标记一个方法,该方法将处理特定类型的异常。默认情况下,它只处理 当前控制器类 中发生的异常

  1. 作用范围:仅处理当前控制器类中抛出的异常。

  2. 异常类型匹配:通过 value 属性指定要处理的异常类型。

  3. 返回值:可以返回视图名称(如 String)、ModelAndView 或直接返回响应体(如 @ResponseBody)。

@ControllerAdvice

@ControllerAdvice 是一个特殊的 @Component,用于定义 @ExceptionHandler@InitBinder@ModelAttribute 方法,这些方法将应用于 所有控制器

核心特性

  1. 全局作用域:处理所有控制器中抛出的异常。

  2. 选择性应用:可通过 annotationsbasePackages 等属性指定要应用的控制器范围。

  3. @ExceptionHandler 结合:在 @ControllerAdvice 类中定义 @ExceptionHandler 方法,实现全局异常处理。

  4. 如果组合使用 @ExceptionHandler@ControllerAdvice,注意Spring MVC 的异常处理遵循 就近原则

    1. 局部处理:优先使用控制器内部的 @ExceptionHandler
    2. 全局处理:如果控制器内部没有匹配的处理方法,则使用 @ControllerAdvice 中的全局处理方法。

    例如,处理流程示例

    1. 控制器抛出UserNotFoundException

      • 首先查找当前控制器中是否有 @ExceptionHandler(UserNotFoundException.class)
      • 如果没有,则查找 @ControllerAdvice 类中是否有匹配的处理方法。
      • 如果仍没有,则由 Spring MVC 的默认异常处理器处理。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      // 全局异常处理器
      @ControllerAdvice
      public class GlobalExceptionHandler {

      // 全局处理 RuntimeException
      @ExceptionHandler(RuntimeException.class)
      public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
      .body("Runtime error: " + ex.getMessage());
      }
      }

      // 特定控制器
      @RestController
      @RequestMapping("/api")
      public class OrderController {

      @PostMapping("/orders")
      public Order createOrder(@RequestBody Order order) {
      // 可能抛出 BusinessException
      return orderService.createOrder(order);
      }

      // 仅处理当前控制器中的 BusinessException
      @ExceptionHandler(BusinessException.class)
      public ResponseEntity<String> handleBusinessException(BusinessException ex) {
      return ResponseEntity.status(HttpStatus.BAD_REQUEST)
      .body("Business error: " + ex.getMessage());
      }
      }
@RestControllerAdvice

@RestControllerAdvice@ControllerAdvice 的特殊版本,是专门为 RESTful 控制器设计的全局异常处理器,它的方法返回值将自动转换为JSON响应体。它是 @ControllerAdvice@ResponseBody 的结合体。

当 Spring Boot 应用启动时,Spring 容器会自动扫描并加载带有 @RestControllerAdvice 注解的类,将其实例化并纳入管理。一旦控制器层在处理请求时抛出异常,Spring MVC 的异常处理机制就会被触发。

在标注了 @RestControllerAdvice 的类中,我们可以定义若干个 @ExceptionHandler 方法,这些方法会根据其参数类型与抛出的异常类型进行匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(YourSelfException.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Internal Server Error",
ex.getMessage()
);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(value = NullPointerException.class)
public ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException ex) {
// 对空指针异常的处理逻辑
}

@ExceptionHandler(value = MyCustomException.class)
public ResponseEntity<ErrorResponse> handleMyCustomException(MyCustomException ex) {
// 对自定义异常 MyCustomException 的处理逻辑
}

@ExceptionHandler(value = Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
// 对所有未特别处理的异常的通用处理逻辑
}
}

由于 @RestControllerAdvice 类型的处理器返回值带有 @ResponseBody 效果,因此,这些 @ExceptionHandler 方法的返回值会被自动转换为 HTTP 响应体的内容。常见的做法是返回一个自定义的错误响应实体,如 ErrorResponse,包含错误代码、消息等信息,方便客户端理解和处理,自定义的错误响应实体示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse {
private int status;
private String error;
private String message;
private String path;
private long timestamp;

public ErrorResponse(int status, String error, String message) {
this.status = status;
this.error = error;
this.message = message;
this.timestamp = System.currentTimeMillis();
}
}
@ResponseStatus

@ResponseStatus注解是spring-web包中提供的一个注解,其主要作用就是为了改变HTTP响应的状态码

有两种用法

  • 一种是加载自定义异常类上

  • 一种是加在目标方法中,当修饰一个类的时候,通常修饰的是一个异常类。

    1
    2
    3
    4
    5
    @RequestMapping(path = "/401")
    @ResponseStatus(value = HttpStatus.CREATED)
    public Response unauthorized() {
    return new Response(401, "Unauthorized", null);
    }

    HttpStatus.CREATED 状态码为201,将原来请求状态码200改为201。

    img

@ResponseStatus注释可指定下表所示属性

属性 类型 是否必要 说明
code HttpStatus http状态码,如HttpStatus.CREATED,HttpStatus.OK
value String 同code属性
reason HttpStatus 错误信息

如果@ResponseStatusreason属性,@RequestMapping方法返回值都不处理了,直接返回服务器自带的 ERROR页面,交互体验比较差,建议不要使用。而且当@ResponseStatus用在方法上,如果添加了reason属性,且reason不为”“,且code > 0(哪怕状态码是200),也会对当前请求走错误处理。

响应状态的覆盖顺序

  1. 方法级@ResponseStatus
  2. 异常类级@ResponseStatus
  3. ResponseEntity设置的状态码
  4. Spring默认状态码

可以通过自定义异常类和 @ResponseStatus 注解来简化异常处理:

使用时,先声明一个自定义异常类,在自定义异常类上面加上@ResponseStatus注释表示系统运行期间,当抛出自定义异常的时候,使用@ResponseStatus注解中声明的属性和reason属性将异常信息返回给客户端,提高可读性。

1
2
3
4
5
6
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(reason="The system is exception!")
public class BusinessException extends RuntimeException
{
}
1
2
3
4
5
6
7
@RequestMapping(value = "/helloResponseStatus2")
public void helloResponseStatus2(@RequestParam(value = "name") String name) {
if (StringUtils.isBlank(name)) {
throw new BusinessException();
}
}

响应结果可以看到:

img

@ResponseStatus注解配合@ExceptionHandler注解使用会更好

1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidationException(ValidationException ex) {
return new ErrorResponse(
"VALIDATION_FAILED",
ex.getFieldErrors()
);
}
}
  • @ExceptionHandler(ValidationException.class)指定该方法处理 ValidationException 类型的异常。当任何控制器抛出 ValidationException 时,此方法会被调用,@ResponseStatus(HttpStatus.BAD_REQUEST)将 HTTP 状态码设置为 400(Bad Request)
  • 这种组合无需在每个异常处理方法中手动设置 HttpServletResponse 的状态码,代码更简洁,符合 RESTful API 设计原则。

而且还可以实现组合状态码与自定义响应

1
2
3
4
5
6
7
8
9
@ExceptionHandler(PaymentRequiredException.class)
@ResponseStatus(HttpStatus.PAYMENT_REQUIRED)
public ApiResponse<Void> handlePaymentRequired(PaymentRequiredException ex) {
return ApiResponse.fail(
"PAYMENT_REQUIRED",
ex.getOrderId(),
"Please complete payment for order " + ex.getOrderId()
);
}

但是会覆盖异常类的注解

1
2
3
4
5
6
7
8
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.GONE) // 覆盖原NOT_FOUND定义
public ErrorResponse handleResourceNotFound(ResourceNotFoundException ex) {
return new ErrorResponse(
"RESOURCE_EXPIRED",
"Resource was permanently removed"
);
}

自定义页面响应

在Web应用中,除了常见的JSON格式错误响应外,很多时候我们还需要返回自定义的错误页面,特别是在传统的服务端渲染(SSR)应用中。Spring Boot提供了多种方式来实现自定义错误页面,让用户获得更友好的错误提示体验。

实现自定义异常页面主要通过以下两种方式:基于静态资源的简单实现基于模板引擎的动态实现

但是我们需要先知道定制错误处理逻辑异常页面的资源加载过程

假设我们拥有自定义的异常页面,如下图结构

img

如上图定义一个 404 的异常界面。查找顺序如下

  • 有模板引擎的情况下:templates/error/状态码,将错误页面命名为 错误状态码.html放在模板引擎文件夹里面的 error 文件夹下, 发生此状态码的错误就会来到对应的页面。
  • 没有模板引擎或者模板引擎找不到错误页面,静态资源 static文件 夹下找:static/error/状态
  • 以上都没有,默认来到 Spring Boot 默认的错误提示页面

也就是说,根据默认错误页面机制,Spring Boot 默认会在以下路径查找错误页面:

  • /error/404.html (针对404错误)
  • /error/5xx.html (针对5xx系列错误)
  • /error/error.html (通用错误页面)

如果需要修改默认的错误页面路径,可以在application.properties中配置:

1
2
server.error.path=/my-error
spring.mvc.static-path-pattern=/static/**

对于更复杂的场景,我们可以使用模板引擎动态渲染错误页面。

以使用Thymeleaf模板引擎为例子

实现步骤:

  1. resources/templates/error/目录下创建模板文件
  2. 模板中可以访问错误信息变量

而 Spring Boot 会自动向错误页面提供以下属性:

  • timestamp:错误发生的时间戳
  • status:HTTP状态码
  • error:错误原因
  • exception:异常类名
  • message:异常消息
  • path:请求路径
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- resources/templates/error/error.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${status} + ' ' + ${error}">Error</title>
</head>
<body>
<h1 th:text="${status} + ' ' + ${error}">Error</h1>
<p th:text="${message}">Error message</p>
<p th:text="${timestamp}">Timestamp</p>
<p th:text="${path}">Path</p>
</body>
</html>

如果需要更灵活的错误页面解析逻辑,可以自定义 ErrorViewResolver

  • 创建自定义 ErrorViewResolver 实现 ErrorViewResolver 接口,并重写 resolveErrorView 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.boot.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@Component
public class CustomErrorViewResolver implements ErrorViewResolver {

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 根据不同的状态码返回不同的视图
if (status == HttpStatus.NOT_FOUND) {
return new ModelAndView("custom/404", model); // 指向 templates/custom/404.html
} else if (status == HttpStatus.INTERNAL_SERVER_ERROR) {
return new ModelAndView("custom/500", model); // 指向 templates/custom/500.html
}
return null; // 返回 null 让默认解析器处理其他情况
}
}

如果需要完全控制错误处理逻辑,可以自定义 ErrorController

  • 创建自定义 ErrorController 实现 ErrorController 接口,处理 /error 路径。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @Controller
    public class EnhancedErrorController implements ErrorController {

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request, Model model) {
    // 收集错误信息
    Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
    Object exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

    // 设置模型数据
    model.addAttribute("timestamp", new Date());
    model.addAttribute("status", status);
    model.addAttribute("error", HttpStatus.valueOf(Integer.valueOf(status.toString())).getReasonPhrase());

    if (exception != null) {
    Throwable ex = (Throwable) exception;
    model.addAttribute("message", ex.getMessage());
    model.addAttribute("exception", ex.getClass().getName());
    }

    Object path = request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
    if (path != null) {
    model.addAttribute("path", path);
    }

    return "error/custom-error";
    }
    }

虽然@ControllerAdvice通常用于返回JSON响应,但也可以用来返回错误页面。算是冷知识了。

需要针对特定异常特殊处理时,可以使用@ControllerAdvice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ModelAndView handleResourceNotFound(ResourceNotFoundException ex) {
ModelAndView mav = new ModelAndView();
mav.addObject("message", ex.getMessage());
mav.addObject("status", HttpStatus.NOT_FOUND.value());
mav.setViewName("error/resource-not-found");
return mav;
}

@ExceptionHandler(Exception.class)
public ModelAndView handleAllExceptions(Exception ex) {
ModelAndView mav = new ModelAndView();
mav.addObject("message", "An unexpected error occurred");
mav.addObject("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
mav.setViewName("error/generic-error");
return mav;
}
}

Spring Boot 异常处理实战

前后分离场景

  • 核心手段:利用 @ControllerAdvice + @ExceptionHandler 组合,对后台产生的所有错误,执行统一异常处理流程,将异常信息以合适格式(如 JSON)返回给前端,便于前端做错误提示等处理。

服务端页面渲染场景

不可预知的 HTTP 码错误(服务器或客户端错误 )

  • 精确匹配:在 classpath:/templates/error/ 路径下,放置像 500.html(处理 500 错误 )、404.html(处理 404 错误 )这类,与具体 HTTP 错误码精准对应的错误页面。
  • 模糊匹配:同样在 classpath:/templates/error/ 路径下,放 5xx.html(匹配所有 5 开头的服务器错误 )、4xx.html(匹配所有 4 开头的客户端错误 )这类通用错误页面,作为精确匹配未命中时的兜底。

业务错误

  • 核心业务错误:针对核心业务里的各类错误,通过代码主动控制跳转,导向专门定制的专属错误页,保证核心业务错误提示精准、贴合业务逻辑。
  • 通用业务错误:借助 classpath:/templates/error.html 页面,在通用业务出错时,展示错误信息,作为通用业务错误的统一展示入口 。

所以就是整体是在 Spring Boot 框架下,依据不同的开发场景(前后端交互形式、错误类型),规划对应的错误处理与页面 / 信息返回策略,让错误处理更规范、体验更优 。

HttpStatus

文章末尾,贴一下org.springframework包内的 HttpStatus状态码的枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.http;

import org.springframework.lang.Nullable;

public enum HttpStatus implements HttpStatusCode {
CONTINUE(100, HttpStatus.Series.INFORMATIONAL, "Continue"),
SWITCHING_PROTOCOLS(101, HttpStatus.Series.INFORMATIONAL, "Switching Protocols"),
PROCESSING(102, HttpStatus.Series.INFORMATIONAL, "Processing"),
EARLY_HINTS(103, HttpStatus.Series.INFORMATIONAL, "Early Hints"),
/** @deprecated */
@Deprecated(
since = "6.0.5"
)
CHECKPOINT(103, HttpStatus.Series.INFORMATIONAL, "Checkpoint"),
OK(200, HttpStatus.Series.SUCCESSFUL, "OK"),
CREATED(201, HttpStatus.Series.SUCCESSFUL, "Created"),
ACCEPTED(202, HttpStatus.Series.SUCCESSFUL, "Accepted"),
NON_AUTHORITATIVE_INFORMATION(203, HttpStatus.Series.SUCCESSFUL, "Non-Authoritative Information"),
NO_CONTENT(204, HttpStatus.Series.SUCCESSFUL, "No Content"),
RESET_CONTENT(205, HttpStatus.Series.SUCCESSFUL, "Reset Content"),
PARTIAL_CONTENT(206, HttpStatus.Series.SUCCESSFUL, "Partial Content"),
MULTI_STATUS(207, HttpStatus.Series.SUCCESSFUL, "Multi-Status"),
ALREADY_REPORTED(208, HttpStatus.Series.SUCCESSFUL, "Already Reported"),
IM_USED(226, HttpStatus.Series.SUCCESSFUL, "IM Used"),
MULTIPLE_CHOICES(300, HttpStatus.Series.REDIRECTION, "Multiple Choices"),
MOVED_PERMANENTLY(301, HttpStatus.Series.REDIRECTION, "Moved Permanently"),
FOUND(302, HttpStatus.Series.REDIRECTION, "Found"),
/** @deprecated */
@Deprecated
MOVED_TEMPORARILY(302, HttpStatus.Series.REDIRECTION, "Moved Temporarily"),
SEE_OTHER(303, HttpStatus.Series.REDIRECTION, "See Other"),
NOT_MODIFIED(304, HttpStatus.Series.REDIRECTION, "Not Modified"),
/** @deprecated */
@Deprecated
USE_PROXY(305, HttpStatus.Series.REDIRECTION, "Use Proxy"),
TEMPORARY_REDIRECT(307, HttpStatus.Series.REDIRECTION, "Temporary Redirect"),
PERMANENT_REDIRECT(308, HttpStatus.Series.REDIRECTION, "Permanent Redirect"),
BAD_REQUEST(400, HttpStatus.Series.CLIENT_ERROR, "Bad Request"),
UNAUTHORIZED(401, HttpStatus.Series.CLIENT_ERROR, "Unauthorized"),
PAYMENT_REQUIRED(402, HttpStatus.Series.CLIENT_ERROR, "Payment Required"),
FORBIDDEN(403, HttpStatus.Series.CLIENT_ERROR, "Forbidden"),
NOT_FOUND(404, HttpStatus.Series.CLIENT_ERROR, "Not Found"),
METHOD_NOT_ALLOWED(405, HttpStatus.Series.CLIENT_ERROR, "Method Not Allowed"),
NOT_ACCEPTABLE(406, HttpStatus.Series.CLIENT_ERROR, "Not Acceptable"),
PROXY_AUTHENTICATION_REQUIRED(407, HttpStatus.Series.CLIENT_ERROR, "Proxy Authentication Required"),
REQUEST_TIMEOUT(408, HttpStatus.Series.CLIENT_ERROR, "Request Timeout"),
CONFLICT(409, HttpStatus.Series.CLIENT_ERROR, "Conflict"),
GONE(410, HttpStatus.Series.CLIENT_ERROR, "Gone"),
LENGTH_REQUIRED(411, HttpStatus.Series.CLIENT_ERROR, "Length Required"),
PRECONDITION_FAILED(412, HttpStatus.Series.CLIENT_ERROR, "Precondition Failed"),
PAYLOAD_TOO_LARGE(413, HttpStatus.Series.CLIENT_ERROR, "Payload Too Large"),
/** @deprecated */
@Deprecated
REQUEST_ENTITY_TOO_LARGE(413, HttpStatus.Series.CLIENT_ERROR, "Request Entity Too Large"),
URI_TOO_LONG(414, HttpStatus.Series.CLIENT_ERROR, "URI Too Long"),
/** @deprecated */
@Deprecated
REQUEST_URI_TOO_LONG(414, HttpStatus.Series.CLIENT_ERROR, "Request-URI Too Long"),
UNSUPPORTED_MEDIA_TYPE(415, HttpStatus.Series.CLIENT_ERROR, "Unsupported Media Type"),
REQUESTED_RANGE_NOT_SATISFIABLE(416, HttpStatus.Series.CLIENT_ERROR, "Requested range not satisfiable"),
EXPECTATION_FAILED(417, HttpStatus.Series.CLIENT_ERROR, "Expectation Failed"),
I_AM_A_TEAPOT(418, HttpStatus.Series.CLIENT_ERROR, "I'm a teapot"),
/** @deprecated */
@Deprecated
INSUFFICIENT_SPACE_ON_RESOURCE(419, HttpStatus.Series.CLIENT_ERROR, "Insufficient Space On Resource"),
/** @deprecated */
@Deprecated
METHOD_FAILURE(420, HttpStatus.Series.CLIENT_ERROR, "Method Failure"),
/** @deprecated */
@Deprecated
DESTINATION_LOCKED(421, HttpStatus.Series.CLIENT_ERROR, "Destination Locked"),
UNPROCESSABLE_ENTITY(422, HttpStatus.Series.CLIENT_ERROR, "Unprocessable Entity"),
LOCKED(423, HttpStatus.Series.CLIENT_ERROR, "Locked"),
FAILED_DEPENDENCY(424, HttpStatus.Series.CLIENT_ERROR, "Failed Dependency"),
TOO_EARLY(425, HttpStatus.Series.CLIENT_ERROR, "Too Early"),
UPGRADE_REQUIRED(426, HttpStatus.Series.CLIENT_ERROR, "Upgrade Required"),
PRECONDITION_REQUIRED(428, HttpStatus.Series.CLIENT_ERROR, "Precondition Required"),
TOO_MANY_REQUESTS(429, HttpStatus.Series.CLIENT_ERROR, "Too Many Requests"),
REQUEST_HEADER_FIELDS_TOO_LARGE(431, HttpStatus.Series.CLIENT_ERROR, "Request Header Fields Too Large"),
UNAVAILABLE_FOR_LEGAL_REASONS(451, HttpStatus.Series.CLIENT_ERROR, "Unavailable For Legal Reasons"),
INTERNAL_SERVER_ERROR(500, HttpStatus.Series.SERVER_ERROR, "Internal Server Error"),
NOT_IMPLEMENTED(501, HttpStatus.Series.SERVER_ERROR, "Not Implemented"),
BAD_GATEWAY(502, HttpStatus.Series.SERVER_ERROR, "Bad Gateway"),
SERVICE_UNAVAILABLE(503, HttpStatus.Series.SERVER_ERROR, "Service Unavailable"),
GATEWAY_TIMEOUT(504, HttpStatus.Series.SERVER_ERROR, "Gateway Timeout"),
HTTP_VERSION_NOT_SUPPORTED(505, HttpStatus.Series.SERVER_ERROR, "HTTP Version not supported"),
VARIANT_ALSO_NEGOTIATES(506, HttpStatus.Series.SERVER_ERROR, "Variant Also Negotiates"),
INSUFFICIENT_STORAGE(507, HttpStatus.Series.SERVER_ERROR, "Insufficient Storage"),
LOOP_DETECTED(508, HttpStatus.Series.SERVER_ERROR, "Loop Detected"),
BANDWIDTH_LIMIT_EXCEEDED(509, HttpStatus.Series.SERVER_ERROR, "Bandwidth Limit Exceeded"),
NOT_EXTENDED(510, HttpStatus.Series.SERVER_ERROR, "Not Extended"),
NETWORK_AUTHENTICATION_REQUIRED(511, HttpStatus.Series.SERVER_ERROR, "Network Authentication Required");

private static final HttpStatus[] VALUES = values();
private final int value;
private final Series series;
private final String reasonPhrase;

private HttpStatus(int value, Series series, String reasonPhrase) {
this.value = value;
this.series = series;
this.reasonPhrase = reasonPhrase;
}

public int value() {
return this.value;
}

public Series series() {
return this.series;
}

public String getReasonPhrase() {
return this.reasonPhrase;
}

public boolean is1xxInformational() {
return this.series() == HttpStatus.Series.INFORMATIONAL;
}

public boolean is2xxSuccessful() {
return this.series() == HttpStatus.Series.SUCCESSFUL;
}

public boolean is3xxRedirection() {
return this.series() == HttpStatus.Series.REDIRECTION;
}

public boolean is4xxClientError() {
return this.series() == HttpStatus.Series.CLIENT_ERROR;
}

public boolean is5xxServerError() {
return this.series() == HttpStatus.Series.SERVER_ERROR;
}

public boolean isError() {
return this.is4xxClientError() || this.is5xxServerError();
}

public String toString() {
int var10000 = this.value;
return "" + var10000 + " " + this.name();
}

public static HttpStatus valueOf(int statusCode) {
HttpStatus status = resolve(statusCode);
if (status == null) {
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
} else {
return status;
}
}

@Nullable
public static HttpStatus resolve(int statusCode) {
HttpStatus[] var1 = VALUES;
int var2 = var1.length;

for(int var3 = 0; var3 < var2; ++var3) {
HttpStatus status = var1[var3];
if (status.value == statusCode) {
return status;
}
}

return null;
}

public static enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);

private final int value;

private Series(int value) {
this.value = value;
}

public int value() {
return this.value;
}

/** @deprecated */
@Deprecated
public static Series valueOf(HttpStatus status) {
return status.series;
}

public static Series valueOf(int statusCode) {
Series series = resolve(statusCode);
if (series == null) {
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
} else {
return series;
}
}

@Nullable
public static Series resolve(int statusCode) {
int seriesCode = statusCode / 100;
Series[] var2 = values();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
Series series = var2[var4];
if (series.value == seriesCode) {
return series;
}
}

return null;
}
}
}