Spring | 如何在项目中优雅的处理异常 - 全局异常处理以及自定义异常处理

引言

在快速迭代和持续交付的今天,软件的健壮性、可靠性和用户体验已经成为区别成功与否的关键因素。特别是在Spring框架中,由于其广泛的应用和丰富的功能,如何优雅地处理异常就显得尤为重要。本文旨在探讨在Spring中如何更加高效、准确和优雅地处理异常,帮助开发者更好地构建和维护Spring应用。

目的与背景

通过本文,读者将深入了解Spring框架中的异常处理机制和策略,学习如何利用Spring提供的工具和注解来实现优雅的异常处理,从而提高软件的可用性和用户满意度。

文章结构概述

本文首先会简要介绍异常处理的基础知识和其在软件开发中的重要性。接着,我们会深入探讨Spring内置的异常处理机制,包括@ExceptionHandler@ControllerAdviceResponseEntityExceptionHandlerErrorController等,并通过实战演示和代码示例来展示如何在实际项目中运用这些机制。

在此基础上,我们还会探讨如何自定义异常处理策略,设计统一的异常响应格式,以及创建和管理业务相关的异常类。此外,文章还会详细讨论状态码与异常的关联,异常日志记录的最佳实践,全局与局部的异常处理策略,以及异常处理的测试策略。

以下所有示例均已上传至Github上,大家可以将项目拉取到本地进行运行

Github示例(如果对Gradle还不熟练,建议翻看我之前的文章):gradle-spring-boot-demo


异常处理的基础知识

在深入探讨Spring的异常处理机制和策略之前,了解和掌握异常处理的基础知识 是至关重要的。异常,是程序在运行时可能遇到的不正常情况 ,它可能导致程序行为的偏差或者终止。在Java中,异常是通过Exception类或其子类来表示的,并且必须被捕获和处理。正确处理异常不仅可以提升程序的健壮性稳定性,优化用户体验,还可以避免可能出现的数据丢失或系统崩溃。

1.1 异常的分类

Java中的异常主要分为受检异常非受检异常

  • 受检异常 :受检异常是那些在编译时期,编译器要求我们必须处理的异常。这类异常常由外部因素引起,例如文件未找到、网络连接失败等。开发者必须在代码中显式地捕获并处理这类异常,或通过throws关键字声明将异常抛出。

    java 复制代码
    public void readFile() {
        try {
            FileInputStream fileInputStream = new FileInputStream("nonexistent.file");
        } catch (FileNotFoundException e) {
            e.printStackTrace(); // 或者进行其他的异常处理,如记录日志、抛出新的异常等。
        }
    }
    // 或者
    public void readFile() throws FileNotFoundException {
        FileInputStream fileInputStream = new FileInputStream("nonexistent.file");
    }
  • 非受检异常 :非受检异常,也称为运行时异常,常由程序逻辑错误引起,如空指针、数组越界等。对于这类异常,Java编译器不会强制我们处理,但在实际开发中,捕获并合理处理这类异常是很重要的。作为程序员,这个异常也是容易犯错的地方,因此要把握好边界

    java 复制代码
    public void calculateLength() {
        String str = null;
        try {
            int length = str.length(); // 这会抛出NullPointerException
        } catch (NullPointerException e) {
            e.printStackTrace(); // 或者进行其他的异常处理,如记录日志、抛出新的异常等。
        }
    }

小结

通过深入了解异常的基础知识、概念分类,我们可以更为准确、高效地处理异常,从而更好地利用Spring框架所提供的异常处理能力。这为我们在后续章节中更进一步地学习和实践Spring中的异常处理机制奠定了基础。


Spring内置的异常处理机制

Spring框架为我们提供了一套丰富而完善的异常处理机制,这套机制允许我们在发生异常时能够做出快速且正确的响应,确保程序的稳定性和用户体验。本章我们将探讨Spring中的主要异常处理机制。

2.1 @ExceptionHandler

@ExceptionHandler注解用于在控制器(Controller)内处理异常。这个注解通常与特定的异常类一起使用,用于处理控制器中可能抛出的该异常。通过@ExceptionHandler,我们可以将异常映射到特定的处理方法,返回定制的错误响应。

2.1.1 使用示例
java 复制代码
@RestController
public class MyController {

    @GetMapping("/endpoint")
    public String endPoint() throws Exception {
        throw new Exception("异常出错!");
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleMyException(Exception e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

话不多说,我们启动项目,访问http://127.0.0.1:8080/endpoint 页面

我在这里抛出了异常,紧接着异常就被捕获到了:

2.2 @ControllerAdvice

@ControllerAdvice是一个全局异常处理注解,它可以捕获所有控制器中抛出的异常。与@ExceptionHandler结合使用,可以实现全局的异常处理策略,保持错误响应的一致性。

2.2.1 使用示例

我们把上面异常捕获注释掉,并添加如下代码:

java 复制代码
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(MyException.class)
    public ResponseEntity<String> handleMyException(MyException e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

同样的访问http://127.0.0.1:8080/endpoint 页面,异常被该类捕获到了

页面输出如下内容:

2.3 ResponseEntityExceptionHandler

ResponseEntityExceptionHandler是一个基础类,我们可以通过继承这个类并覆盖其中的方法,来处理由Spring内部抛出的一系列标准异常,例如MethodArgumentNotValidException等。

2.3.1 使用示例
java 复制代码
@RestControllerAdvice
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatusCode status,
            WebRequest request){

        // 获取所有的错误信息
        List<String> errorDetails = ex.getBindingResult()
                                      .getFieldErrors()
                                      .stream()
                                      .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
                                      .collect(Collectors.toList());

        // 创建一个错误响应体对象
        ApiError apiError = new ApiError(
            LocalDateTime.now(),
            HttpStatus.BAD_REQUEST,
            "Validation Failed",
            errorDetails
        );

        // 返回定制的错误响应体
        return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST);
    }
}

MyController添加如下方法:

java 复制代码
    @PostMapping("/user")
    public ResponseEntity<String> createUser(@Valid @RequestBody User user) {
        return new ResponseEntity<>("User created successfully!" + user.toString(), HttpStatus.CREATED);
    }

写一个测试用例:

java 复制代码
    /**
     * 参数校验
     * {@link MyController#createUser}
     */
    @Test
    void shouldReturnBadRequestWhenNameIsBlank() throws Exception {
        String userJson = "{\"name\":\"\"}"; // 空的name应该触发验证失败

        mockMvc.perform(post("/user")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(userJson))
                .andExpect(status().isBadRequest());
    }

执行!参数校验异常被成功捕获到

咳咳,这样控制台没办法打印,我们使用postman来看下,执行结果如下:

参数校验异常被捕获到了,非常清晰:

2.4 ErrorController

通过实现ErrorController接口,我们可以定制错误映射和错误页面,为用户提供更友好的错误提示。

2.4.1 使用示例
java 复制代码
@RestController
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public ResponseEntity<Map<String, Object>> handleError() {
        // Customize the error response
        return new ResponseEntity<>(...);
    }
    
    @Override
    public String getErrorPath() {
        return "/error";
    }
}

小结

Spring提供的内置异常处理机制,如@ExceptionHandler@ControllerAdviceResponseEntityExceptionHandlerErrorController,允许我们针对不同场景和需求,实现灵活且全面的异常处理策略。通过熟练运用这些工具,我们可以构建出更加稳定、健壮且用户友好的应用。


自定义异常处理

虽然Spring提供了一套丰富的异常处理机制,但在某些情况下,我们可能会需要更加个性化和灵活的异常处理策略。在这种情况下,我们可以通过自定义异常处理来满足我们的需求。以下,我们将探讨如何在Spring中实现自定义异常处理。

3.1 定义自定义异常

自定义异常通常继承自RuntimeExceptionException。通过创建自定义异常,我们可以更精确地表达和捕获特定的错误情况。

3.1.1 创建自定义异常
java 复制代码
public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}

3.2 自定义异常处理器

自定义的异常处理器可以使用@ExceptionHandler@ControllerAdvice来实现,这使我们可以有更多的控制权来定制异常的响应。

3.2.1 创建自定义异常处理器
java 复制代码
@ControllerAdvice
public class CustomExceptionHandler {
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<String> handleCustomException(CustomException e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

3.3 自定义错误响应

我们还可以定制异常的响应格式,例如,可以包含错误代码、错误消息、时间戳等,以提供更多的错误信息。

3.3.1 定义错误响应类
java 复制代码
public class ErrorResponse {
    private int status;
    private String message;
    private long timestamp;
    
    // Constructors, getters and setters
}
3.3.2 返回自定义错误响应
java 复制代码
@ControllerAdvice
public class CustomExceptionHandler {
    
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage(), System.currentTimeMillis());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

小结

通过自定义异常处理,我们可以构建出更加精确和灵活的异常处理策略,以满足特定的业务需求。自定义异常、异常处理器和错误响应允许我们全面掌控异常处理的每个环节,实现真正意义上的个性化异常处理。


状态码与异常

在Web应用中,HTTP状态码是服务端向客户端报告请求结果的一种重要方式。通过合适的状态码,服务端可以明确地告知客户端请求是成功还是失败,以及失败的原因。下面,我们将详细讨论如何在Spring中正确使用HTTP状态码来表示异常。

4.1 HTTP状态码概述

HTTP状态码由三位数字组成,其中第一位数字定义了状态码的类型。常见的状态码类型包括:

  • 2xx:成功。表示请求已被成功接收、理解和接受。
  • 4xx:客户端错误。表示客户端似乎有错误,例如,无效的请求或无法找到资源。
  • 5xx:服务器错误。表示服务器未能完成明显有效的请求。

4.2 状态码与异常的关系

在Spring中,我们通常使用ResponseEntity来表示HTTP响应,其中包含了状态码和响应体。当发生异常时,我们应该返回代表错误的状态码,如400 Bad Request500 Internal Server Error,并在响应体中提供错误的详细信息。

4.2.1 使用ResponseEntity返回状态码
java 复制代码
@RestController
public class MyController {

    @GetMapping("/myEndpoint")
    public ResponseEntity<String> myEndpoint() {
        // ...
        return new ResponseEntity<>("Error Message", HttpStatus.BAD_REQUEST);
    }
}

4.3 使用@ResponseStatus定义状态码

@ResponseStatus注解允许我们在异常类或处理方法上直接指定HTTP状态码。当该异常被抛出时,Spring会自动使用指定的状态码作为HTTP响应的状态码。

4.3.1 在异常类上使用@ResponseStatus
java 复制代码
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Resource not found")
public class ResourceNotFoundException extends RuntimeException {
}
4.3.2 在处理方法上使用@ResponseStatus
java 复制代码
@RestController
public class MyController {

    @GetMapping("/myEndpoint")
    public String myEndpoint() {
        // ...
        throw new ResourceNotFoundException();
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String handleResourceNotFoundException() {
        return "Resource not found";
    }
}

小结

正确使用HTTP状态码可以使我们的应用更加符合HTTP协议,也使客户端更容易理解响应的含义。通过ResponseEntity@ResponseStatus,我们可以灵活地为异常指定合适的状态码,从而实现更加准确和清晰的错误报告。


异常处理的最佳实践

在Spring中,细致而明智地处理异常是至关重要的,因为它直接影响到软件的稳定性和用户体验。这一节将通过实例探讨一些在Spring中处理异常的最佳实践。

5.1 准确的异常类型

正确选择异常类型是关键。例如,当遇到无效用户输入时,应该选择IllegalArgumentException而非一般的RuntimeException。具体的异常类型可以给予更多的上下文,助力快速定位问题。

java 复制代码
if (userInput == null) {
    throw new IllegalArgumentException("User input cannot be null");
}

5.2 自定义异常

当内置异常无法精确表达问题时,应定义自定义异常。这些异常应该扩展自RuntimeException或其他适合的父类,并清晰地表达异常的本质。

java 复制代码
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String userId) {
        super("User with ID " + userId + " not found");
    }
}

5.3 清晰准确的错误信息

错误信息必须清晰且准确。它们应该提供足够的细节来帮助理解问题所在,避免模糊且无用的描述。

java 复制代码
throw new UserNotFoundException(userId);

5.4 妥善处理异常堆栈

异常堆栈包含宝贵的调试信息。在开发环境下,我们应记录完整的异常堆栈,但在生产环境,应避免将详细的异常堆栈暴露给用户。

java 复制代码
logger.error("Exception occurred", exception);

5.5 使用HTTP状态码

返回合适的HTTP状态码是通信的基础。例如,400 Bad Request应该用于无效的用户输入,而500 Internal Server Error用于服务器错误。

java 复制代码
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid user input");

5.6 日志记录

每一处异常都应被适当地记录,携带异常类型、信息和堆栈。日志应足够详细,以支持快速的问题定位和解决。

java 复制代码
logger.error("User not found: {}", userId, exception);

5.7 测试异常处理

异常处理逻辑也需经过严格测试,通过单元和集成测试来保证逻辑的正确性和健壮性。

java 复制代码
@Test
void shouldThrowUserNotFoundException() {
    Assertions.assertThrows(UserNotFoundException.class, () -> {
        userService.findUser("invalidId");
    });
}

小结

遵循这些最佳实践能够确保你的异常处理逻辑既清晰又准确,进而降低错误率并提升用户满意度。记住,优雅的异常处理不仅可以简化开发工作,而且能在问题出现时提供有力支持。


总结

在开发复杂的Spring应用程序时,异常处理是不可或缺的一环。合理而有效的异常处理不仅能够提高应用程序的健壮性和稳定性,还能够优化用户体验,减少开发和维护的难度。

在Spring中,有效的异常处理要求我们深入理解异常处理机制、策略和最佳实践。我们需要细心地设计和测试我们的异常处理逻辑,确保它们能够在实际运行中满足预期,为用户提供友好而准确的错误信息,同时也为开发者提供足够的信息来定位和解决问题。希望本文能够帮助读者更好地理解Spring中的异常处理,以及如何设计和实施有效的异常处理策略。


参考文献

  1. Spring、SpringBoot统一异常处理的3种方法 - CSDN
  2. Spring Boot 全局异常处理整理!开发必会! - 知乎
  3. Spring Boot项目优雅的全局异常处理方式(全网最新) - CSDN
  4. 基于Spring Cloud Gateway 的统一异常处理 - 掘金
  5. Spring Cloud 如何统一异常处理?写得太好了! - 腾讯云
相关推荐
weixin_462428478 分钟前
使用 Caffeine 缓存并在业务方法上通过注解实现每3到5秒更新缓存
java·缓存
程序媛小果10 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
骑鱼过海的猫12311 分钟前
【java】java通过s3访问ceph报错
java·ceph·iphone
杨充17 分钟前
13.观察者模式设计思想
java·redis·观察者模式
Lizhihao_20 分钟前
JAVA-队列
java·开发语言
喵叔哟29 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟29 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk32 分钟前
maven环境搭建
java·maven
Daniel 大东1 小时前
idea 解决缓存损坏问题
java·缓存·intellij-idea
wind瑞1 小时前
IntelliJ IDEA插件开发-代码补全插件入门开发
java·ide·intellij-idea