MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Spring Boot中的异常处理与错误管理

2021-01-035.3k 阅读

Spring Boot 异常处理基础

在 Spring Boot 应用开发中,异常处理是确保应用稳定性和可靠性的关键环节。当应用运行过程中出现错误,例如输入不合法、资源不可用或者业务规则被违反时,异常就会抛出。Spring Boot 提供了一套强大且灵活的机制来处理这些异常,使得我们能够优雅地处理错误情况,为用户提供友好的反馈,同时帮助开发者快速定位和解决问题。

1. 内置异常处理机制

Spring Boot 基于 Spring 框架,继承了其成熟的异常处理体系。默认情况下,Spring Boot 会对一些常见的异常进行处理,并返回合适的 HTTP 状态码。例如,当发生 HttpRequestMethodNotSupportedException 异常时,Spring Boot 会返回 HTTP 405 Method Not Allowed 状态码,表示请求的方法不被允许;当发生 HttpMediaTypeNotSupportedException 异常时,会返回 HTTP 415 Unsupported Media Type 状态码,说明请求的媒体类型不被支持。

2. 异常处理器

Spring Boot 提供了几种类型的异常处理器,以满足不同场景下的异常处理需求。

  • @ControllerAdvice + @ExceptionHandler:这是一种非常常用的方式。@ControllerAdvice 注解用于定义一个全局的异常处理类,它可以对所有 @Controller 中抛出的异常进行处理。而 @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;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

在上述代码中,GlobalExceptionHandler 类被 @ControllerAdvice 注解修饰,成为一个全局异常处理类。handleIllegalArgumentException 方法使用 @ExceptionHandler(IllegalArgumentException.class) 注解,表示专门处理 IllegalArgumentException 异常。当应用中某个 @Controller 抛出 IllegalArgumentException 异常时,该方法就会被调用,返回一个包含异常信息和 HTTP 400 Bad Request 状态码的 ResponseEntity

  • HandlerExceptionResolver:这是一个更底层的接口,用于自定义异常处理逻辑。实现该接口可以完全控制异常处理流程,包括如何解析异常、如何生成响应等。Spring Boot 提供了一些默认的实现类,如 DefaultHandlerExceptionResolver,它负责处理许多常见的 Spring 异常。

如果我们想要自定义一个 HandlerExceptionResolver,可以如下实现:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CustomHandlerExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (ex instanceof IllegalArgumentException) {
            ResponseEntity<String> entity = new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
            try {
                response.getWriter().write(entity.getBody());
                response.setStatus(entity.getStatusCodeValue());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return new ModelAndView();
        }
        return null;
    }
}

然后,需要将这个自定义的 HandlerExceptionResolver 注册到 Spring 容器中:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public HandlerExceptionResolver customHandlerExceptionResolver() {
        return new CustomHandlerExceptionResolver();
    }
}

自定义异常处理

在实际开发中,应用通常会遇到各种业务相关的异常情况,这就需要我们自定义异常并进行处理。

1. 自定义异常类

首先,我们需要定义自己的异常类。自定义异常类通常继承自 Exception 或其子类(如 RuntimeException)。如果希望异常在编译时被检查,就继承 Exception;如果是运行时异常,继承 RuntimeException

以下是一个简单的自定义业务异常类示例:

public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {
        super(message);
    }
}

2. 处理自定义异常

有了自定义异常类后,我们可以在全局异常处理类中添加对它的处理逻辑。

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

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

在上述代码中,handleUserNotFoundException 方法专门处理 UserNotFoundException 异常,返回一个包含异常信息和 HTTP 404 Not Found 状态码的 ResponseEntity

3. 在业务代码中抛出异常

在业务代码中,当满足特定条件时,我们就可以抛出自定义异常。

import org.springframework.stereotype.Service;

@Service
public class UserService {

    public void getUserById(Long id) {
        // 假设这里是查询用户的逻辑,如果未找到用户
        if (id == null) {
            throw new UserNotFoundException("User not found with id: " + id);
        }
        // 正常业务逻辑...
    }
}

异常处理中的日志记录

在异常处理过程中,记录详细的日志信息对于排查问题至关重要。Spring Boot 集成了多种日志框架,如 Logback、Log4j 等。默认情况下,Spring Boot 使用 Logback 作为日志框架。

1. 记录异常日志

在异常处理方法中,我们可以使用日志记录工具记录异常的详细信息。以 Logback 为例,在全局异常处理类中添加日志记录:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
        logger.error("User not found exception occurred", ex);
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        logger.error("Illegal argument exception occurred", ex);
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

在上述代码中,使用 logger.error 方法记录异常信息。第一个参数是日志的描述信息,第二个参数是异常对象,这样可以记录异常的堆栈跟踪信息,方便开发者定位问题。

2. 日志级别与配置

Logback 使用日志级别来控制日志的输出。常见的日志级别有 TRACEDEBUGINFOWARNERROR。在开发阶段,我们通常会将日志级别设置为 DEBUGTRACE,以便获取更详细的信息;而在生产环境中,一般设置为 INFOWARN,避免过多的日志输出影响系统性能。

可以通过在 src/main/resources 目录下创建 logback-spring.xml 文件来配置日志级别和输出格式等。以下是一个简单的 logback-spring.xml 配置示例:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

在上述配置中,定义了一个 STDOUT 输出到控制台的 appender,并设置了日志输出的格式。root 标签设置了根日志级别为 info,表示只输出 INFO 级别及以上的日志。

异常处理与 RESTful API

在 Spring Boot 开发 RESTful API 时,异常处理需要特别关注,因为我们需要向客户端返回符合 RESTful 规范的错误响应。

1. 标准错误响应格式

通常,RESTful API 的错误响应会采用 JSON 格式,包含错误码、错误信息等字段。我们可以定义一个统一的错误响应类来封装这些信息。

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {

    private int errorCode;
    private String errorMessage;

    public ErrorResponse(int errorCode, String errorMessage) {
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }

    public int getErrorCode() {
        return errorCode;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}

2. 异常处理返回错误响应

在全局异常处理类中,修改异常处理方法,使其返回统一格式的错误响应。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
        logger.error("User not found exception occurred", ex);
        ErrorResponse errorResponse = new ErrorResponse(40401, ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
        logger.error("Illegal argument exception occurred", ex);
        ErrorResponse errorResponse = new ErrorResponse(40001, ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

在上述代码中,handleUserNotFoundExceptionhandleIllegalArgumentException 方法返回的是 ResponseEntity<ErrorResponse>,将错误信息封装在 ErrorResponse 对象中,并设置合适的 HTTP 状态码。

3. 自定义错误码

在实际应用中,为了更好地区分不同类型的错误,我们通常会自定义错误码。例如,40401 表示用户未找到,40001 表示非法参数等。可以将这些错误码定义在一个常量类中,方便管理和维护。

public class ErrorCodeConstants {

    public static final int USER_NOT_FOUND = 40401;
    public static final int ILLEGAL_ARGUMENT = 40001;
}

然后在异常处理方法中使用这些常量:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
        logger.error("User not found exception occurred", ex);
        ErrorResponse errorResponse = new ErrorResponse(ErrorCodeConstants.USER_NOT_FOUND, ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
        logger.error("Illegal argument exception occurred", ex);
        ErrorResponse errorResponse = new ErrorResponse(ErrorCodeConstants.ILLEGAL_ARGUMENT, ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

异常处理与事务管理

在涉及数据库操作等事务性场景中,异常处理与事务管理紧密相关。Spring Boot 提供了强大的事务管理功能,通常使用 @Transactional 注解来声明事务。

1. 事务回滚与异常

默认情况下,当在一个被 @Transactional 注解的方法中抛出 RuntimeException 或其子类异常时,Spring 会自动回滚事务。而对于 CheckedException(继承自 Exception 但不是 RuntimeException),事务不会自动回滚,除非在 @Transactional 注解中显式设置 rollbackFor 属性。

以下是一个简单的示例:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional
    public void saveUser(User user) {
        String sql = "INSERT INTO users (name, age) VALUES (?,?)";
        jdbcTemplate.update(sql, user.getName(), user.getAge());
        // 假设这里抛出一个运行时异常
        throw new RuntimeException("Simulated error");
    }
}

在上述代码中,saveUser 方法被 @Transactional 注解修饰,当抛出 RuntimeException 时,之前执行的数据库插入操作会被回滚。

2. 自定义事务回滚策略

如果我们希望在抛出特定的 CheckedException 时也回滚事务,可以在 @Transactional 注解中设置 rollbackFor 属性。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    @Transactional(rollbackFor = UserSaveException.class)
    public void saveUser(User user) throws UserSaveException {
        String sql = "INSERT INTO users (name, age) VALUES (?,?)";
        int rowsAffected = jdbcTemplate.update(sql, user.getName(), user.getAge());
        if (rowsAffected <= 0) {
            throw new UserSaveException("Failed to save user");
        }
    }
}

在上述代码中,UserSaveException 是一个自定义的 CheckedException,通过在 @Transactional 注解中设置 rollbackFor = UserSaveException.class,当抛出 UserSaveException 时,事务会被回滚。

3. 异常处理与事务传播行为

事务传播行为定义了一个事务方法被另一个事务方法调用时,事务如何管理。Spring 提供了多种事务传播行为,如 PROPAGATION_REQUIRED(默认)、PROPAGATION_REQUIRES_NEWPROPAGATION_NESTED 等。

在异常处理过程中,事务传播行为会影响异常对事务的处理。例如,当使用 PROPAGATION_REQUIRES_NEW 时,每次调用被 @Transactional 注解的方法都会开启一个新的事务,即使外层方法已经在事务中。如果内层方法抛出异常,只会回滚内层事务,而不会影响外层事务。

以下是一个简单示例展示不同事务传播行为下的异常处理:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionService {

    @Autowired
    private AnotherService anotherService;

    @Transactional(propagation = Propagation.REQUIRED)
    public void outerMethod() {
        try {
            anotherService.innerMethod();
        } catch (Exception e) {
            // 捕获异常并处理
        }
    }
}

@Service
public class AnotherService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void innerMethod() {
        // 假设这里执行一些数据库操作并抛出异常
        throw new RuntimeException("Inner method error");
    }
}

在上述代码中,outerMethod 使用 PROPAGATION_REQUIRED 事务传播行为,innerMethod 使用 PROPAGATION_REQUIRES_NEW 事务传播行为。当 innerMethod 抛出异常时,只有 innerMethod 中的事务会被回滚,outerMethod 的事务不受影响。

异常处理的最佳实践

  1. 分层处理异常:在不同的层次(如控制器层、服务层、数据访问层)进行合适的异常处理。在控制器层主要处理与 HTTP 相关的异常,并向客户端返回友好的错误响应;服务层处理业务逻辑相关的异常,并可能进行一些业务补偿操作;数据访问层处理与数据库操作相关的异常,如连接错误、SQL 语法错误等。

  2. 异常封装与转换:在不同层次之间传递异常时,尽量进行异常封装和转换。将底层的技术异常(如数据库连接异常)转换为业务层能理解的异常,避免将底层技术细节暴露给上层。例如,在数据访问层捕获 SQLException,并转换为自定义的业务异常 DataAccessException 向上层抛出。

  3. 避免空的异常捕获块:空的异常捕获块会隐藏异常信息,使得问题难以排查。如果确实需要捕获异常,应该至少记录详细的异常日志,或者进行适当的处理,如返回默认值、进行业务补偿等。

  4. 使用合适的异常类型:根据异常的性质选择合适的异常类型。如果是编译时需要检查的异常,使用 Exception 及其子类;如果是运行时异常,使用 RuntimeException 及其子类。同时,尽量自定义业务异常类,以提高代码的可读性和可维护性。

  5. 测试异常处理逻辑:编写单元测试和集成测试来验证异常处理逻辑的正确性。确保在各种异常情况下,应用能够返回正确的错误响应,并且不会出现未处理的异常导致应用崩溃。

  6. 监控与告警:在生产环境中,对异常进行监控和告警是非常重要的。可以使用工具如 Prometheus、Grafana 等对异常进行统计和可视化,当异常出现的频率或数量超过一定阈值时,及时发送告警通知,以便开发者能够快速响应和解决问题。

通过遵循这些最佳实践,可以使 Spring Boot 应用的异常处理更加健壮、可靠,提高应用的质量和稳定性。

综上所述,Spring Boot 提供了丰富且灵活的异常处理机制,从内置的异常处理到自定义异常、日志记录、与 RESTful API 和事务管理的结合,以及最佳实践等方面,都为开发者提供了全面的支持。合理运用这些机制,可以有效地处理应用中出现的各种异常情况,提升用户体验,同时帮助开发者快速定位和解决问题,确保应用的稳定运行。在实际开发中,需要根据项目的具体需求和场景,选择合适的异常处理方式,并不断优化和完善异常处理逻辑。