Skip to content

配置讲解-打造异常处理的优雅之道

异常的定义

在Java中,异常是程序执行过程中发生的不正常情况,它打断了正常的指令流。Java提供了一套完整的异常处理框架,帮助开发者有效地处理运行时出现的错误和异常情况,以确保程序的健壮性和稳定性。Java的异常处理机制是建立在Throwable类及其子类的基础上的。

Throwable

Throwable是Java异常层次结构的根类,它是所有错误和异常的基类。在Java中,只有Throwable的实例或其子类的实例才可以被throw关键字抛出,或者被catch关键字捕获。Throwable有两个主要的子类:ErrorException

  • Error:代表编译时和系统错误(外部错误),如内存溢出(OutOfMemoryError)、虚拟机错误(VirtualMachineError)等。这些错误通常是严重的,程序应该尽量避免发生这类错误,一旦发生,程序通常无法处理。
  • Exception:代表程序本身可以处理的异常。它分为两大类:检查型异常(checked exceptions)和非检查型异常(unchecked exceptions,也称为运行时异常)。

Exception

Exception类是所有检查型异常的父类。检查型异常是那些在编译时必须被处理(捕获或声明抛出)的异常。如果方法可能会抛出某个检查型异常,但没有捕获它,那么该方法必须通过throws关键字声明该异常。

检查型异常通常是外部错误,如尝试打开不存在的文件时发生的FileNotFoundException,或网络通信过程中发生的IOException

RuntimeException

RuntimeExceptionException的子类,代表非检查型异常,即运行时异常。运行时异常包括了Java运行时环境可以自动抛出的异常。

与检查型异常不同,运行时异常是由程序逻辑错误引起的,如空指针访问(NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。编译器不要求强制处理或声明抛出运行时异常,因为它们通常是可以通过程序逻辑来预防的错误。

总结来说,Java的异常处理机制是通过Throwable类及其子类实现的。Error用于表示严重的系统级错误,通常是不可恢复的;Exception用于表示程序可以处理的异常,分为检查型异常和非检查型异常(RuntimeException)。正确理解和使用这些异常类对于编写健壮、可靠的Java程序至关重要。

以上将异常的特点介绍完毕,而在Springboot中开发时,需要考虑出现异常时的处理,已经抛出异常后如何将信息返回给前端,这就需要进行异常的统一处理

异常的统一处理

在Spring Boot项目中配置统一的异常处理是一种最佳实践,它不仅能够提升代码的可维护性和可读性,还能够为最终用户提供更加友好和一致的错误响应。通过实现统一异常处理,开发者可以集中处理应用中的各种异常,避免了在每个控制器或服务中重复编写错误处理逻辑,从而使得代码更加简洁和易于管理。下面我们将详细探讨未配置统一异常处理的坏处,以及配置后的好处

没有配置统一异常处理的坏处

  1. 代码重复:在没有统一异常处理的情况下,开发者可能需要在每个控制器或服务中编写异常处理代码,这会导致大量的重复代码,增加了代码的维护成本。
  2. 错误处理不一致:各个模块独立处理异常可能会导致错误响应的格式不一致,给前端开发和最终用户带来困扰,影响用户体验。
  3. 难以维护和扩展:随着项目规模的扩大,异常处理逻辑分散在各处,当需要修改错误响应格式或处理逻辑时,必须逐个修改,效率低下,容易遗漏。
  4. 调试困难:没有一个集中的异常处理机制,当出现错误时,定位和调试问题变得更加困难,特别是在复杂的应用中。
  5. 安全风险:不恰当的异常处理可能会泄露敏感信息给客户端,比如堆栈跟踪信息,这可能会导致安全漏洞。

配置统一异常处理的好处

  1. 减少代码重复:通过集中处理所有异常,可以显著减少代码重复,使得异常处理逻辑更加集中和一致。
  2. 提高代码可维护性:统一的异常处理机制使得代码更加整洁,易于管理和维护。对异常处理逻辑的修改和扩展可以在一个地方进行,提高了开发效率。
  3. 增强用户体验:统一异常处理允许开发者提供一致且友好的错误响应给用户,无论是API的消费者还是最终用户,都能从中受益。
  4. 便于错误监控和日志记录:集中处理异常使得监控和记录日志变得更加方便,有助于快速发现并解决问题。
  5. 提升应用的安全性:可以通过统一异常处理来过滤和处理敏感信息,防止敏感数据泄露给客户端,提升应用的安全性。

举例

为了能让小伙伴体会到异常统一配置的好处,我们来直接举例说明

执行成功情况

java
public UserVo getByMobile(UserMobileDto userMobileDto) {
    LambdaQueryWrapper<UserMobile> queryWrapper = Wrappers.lambdaQuery(UserMobile.class)
            .eq(UserMobile::getMobile, userMobileDto.getMobile());
    UserVo userVo = userMobileMapper.selectOne(queryWrapper);
    return userVo;
}
json
{
    "code": "0",
    "message": "",
    "data": {
        "id": "8408725077058789376",
        "name": "",
        "relName": "",
        "gender": "1",
        "mobile": "132****4769",
        "emailStatus": "0",
        "email": "",
        "relAuthenticationStatus": "0",
        "idNumber": "",
        "address": ""
    }
}

执行失败情况

直接抛出RuntimeException异常来模拟失败情况

java
public UserVo getByMobile(UserMobileDto userMobileDto) {
    throw new RuntimeException("用户为空");
}

使用异常统一处理

json
{
    "code": "-100",
    "message": "系统错误,请稍后重试!",
    "data": null
}

没有异常统一处理

json
{
    "timestamp": "2024-03-03 15:33:50",
    "status": "500",
    "error": "Internal Server Error",
    "message": "",
    "path": "/user/get/mobile"
}

到这里,小伙伴能发现区别了,发现没有处理异常的情况下,返回的数据结构发生了变化。这就是不允许的情况,因为前端分离的情况下,前端调用后端的数据返回的结构无论在成功或者失败的情况,数据结构都必须保持一致,否则前端根本没有办法解析。

设计

在Spring Boot中,异常的统一处理是通过@ControllerAdvice(或@RestControllerAdvice)和@ExceptionHandler注解来实现的。这种机制允许开发者在应用程序中集中处理所有控制器(Controller)层抛出的异常,从而避免了在每个控制器中重复编写异常处理代码,提高了代码的可维护性和一致性。下面详细介绍这两个注解的作用和如何使用它们来进行异常的统一处理。

@RestControllerAdvice

@RestControllerAdvice@ControllerAdvice注解的特化,它结合了@ControllerAdvice@ResponseBody的功能。

@ControllerAdvice注解允许你定义一个全局的异常处理类,该类会应用到所有的@RequestMapping方法上。当使用@RestControllerAdvice时,意味着异常处理方法的返回值会直接作为响应体返回,适用于构建RESTful API。你可以通过指定包名、注解等方式来限定@RestControllerAdvice应用的范围。

@ExceptionHandler

@ExceptionHandler注解用于在@ControllerAdvice@RestControllerAdvice注解的类中定义具体的异常处理方法。它标记的方法可以处理特定类型的异常。当控制器或控制器通知(Controller Advice)中的方法抛出异常时,会匹配带有@ExceptionHandler注解的方法,并调用该方法来处理异常。

实践应用

在实践中,@RestControllerAdvice通常用于定义一个全局的异常处理类,它会捕获并处理所有控制器抛出的异常。在这个类中,可以使用多个@ExceptionHandler方法来处理不同类型的异常。这样,当应用抛出异常时,可以根据异常类型自动跳转到相应的处理方法,然后返回一个友好的错误信息给客户端,从而提升用户体验。

例如,可以创建一个全局异常处理类,使用@RestControllerAdvice注解标注,并在其中定义多个用@ExceptionHandler注解的方法,分别处理常见的异常,返回统一格式的错误响应。

下面就来实现异常处理的配置

DefaultExceptionHandler

java
@Slf4j
@RestControllerAdvice
public class DefaultExceptionHandler {

    /**
    * 业务异常
    * */
    @ExceptionHandler(value = DaMaiFrameException.class)
    public ApiResponse<String> toolkitExceptionHandler(HttpServletRequest request, DaMaiFrameException daMaiFrameException) {
        log.error("业务异常 错误信息 : {} method : {} url : {} query : {} ", daMaiFrameException.getMessage(), request.getMethod(), getRequestUrl(request), getRequestQuery(request), daMaiFrameException);
        return ApiResponse.error(daMaiFrameException.getCode(), daMaiFrameException.getMessage());
    }
    /**
     * 参数验证异常
     */
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ApiResponse<List<ArgumentError>> validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
        log.error("参数验证异常 错误信息 : {} method : {} url : {} query : {} ", ex.getMessage(), request.getMethod(), getRequestUrl(request), getRequestQuery(request), ex);
        BindingResult bindingResult = ex.getBindingResult();
        List<ArgumentError> argumentErrorList = 
                bindingResult.getFieldErrors()
                        .stream()
                        .map(fieldError -> {
                            ArgumentError argumentError = new ArgumentError();
                            argumentError.setArgumentName(fieldError.getField());
                            argumentError.setMessage(fieldError.getDefaultMessage());
                            return argumentError;
                        }).collect(Collectors.toList());
        return ApiResponse.error(BaseCode.PARAMETER_ERROR.getCode(),argumentErrorList);
    }

    /**
     * 拦截未捕获异常
     */
    @ExceptionHandler(value = Throwable.class)
    public ApiResponse<String> defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
        log.error("全局异常 错误信息 : {} method : {} url : {} query : {} ", throwable.getMessage(), request.getMethod(), getRequestUrl(request), getRequestQuery(request), throwable);
        return ApiResponse.error();
    }

    private String getRequestUrl(HttpServletRequest request) {
        return request.getRequestURL().toString();
    }

    private String getRequestQuery(HttpServletRequest request){
        return request.getQueryString();
    }
}

可以看到,第一个拦截处理的异常是 DaMaiFrameException,第二个拦截处理的异常是MethodArgumentNotValidException,并使用ApiResponse.error来将错误信息返回给前端,关于ApiResponse.error的详细介绍,可跳转到相应文档

配置讲解-打造标准化数据返回结构

接下来,我们来依次介绍这些拦截处理的异常作用

DaMaiFrameException

java
@Data
public class DaMaiFrameException extends BaseException {

	private Integer code;
	
	private String message;

	public DaMaiFrameException() {
		super();
	}

	public DaMaiFrameException(String message) {
		super(message);
	}
	
	
	public DaMaiFrameException(String code, String message) {
		super(message);
		this.code = Integer.parseInt(code);
		this.message = message;
	}
	
	public DaMaiFrameException(Integer code, String message) {
		super(message);
		this.code = code;
		this.message = message;
	}
	
	public DaMaiFrameException(BaseCode baseCode) {
		super(baseCode.getMsg());
		this.code = baseCode.getCode();
		this.message = baseCode.getMsg();
	}
	
	public DaMaiFrameException(ApiResponse apiResponse) {
		super(apiResponse.getMessage());
		this.code = apiResponse.getCode();
		this.message = apiResponse.getMessage();
	}

	public DaMaiFrameException(Throwable cause) {
		super(cause);
	}

	public DaMaiFrameException(String message, Throwable cause) {
		super(message, cause);
		this.message = message;
	}

	public DaMaiFrameException(Integer code, String message, Throwable cause) {
		super(message, cause);
		this.code = code;
		this.message = message;
	}
}

DaMaiFrameException 是专门用来处理业务异常,也就是当业务验证出现要终止的情况或者调用业务服务api出现异常时,来使用此异常,我们来举例说明

java
public UserVo getByMobile(UserMobileDto userMobileDto) {
    LambdaQueryWrapper<UserMobile> queryWrapper = Wrappers.lambdaQuery(UserMobile.class)
            .eq(UserMobile::getMobile, userMobileDto.getMobile());
    UserMobile userMobile = userMobileMapper.selectOne(queryWrapper);
    if (Objects.isNull(userMobile)) {
        throw new DaMaiFrameException(BaseCode.USER_MOBILE_EMPTY);
    }
    User user = userMapper.selectById(userMobile.getUserId());
    if (Objects.isNull(user)) {
        throw new DaMaiFrameException(BaseCode.USER_EMPTY);
    }
    UserVo userVo = new UserVo();
    BeanUtil.copyProperties(user,userVo);
    userVo.setMobile(userMobile.getMobile());
    return userVo;
}
java
public enum BaseCode {
    USER_MOBILE_EMPTY(20002,"用户手机号不存在"),
    USER_EMPTY(60004,"用户不存在"),
}

在这个例子中,当用户手机数据不存在时,抛出了用户手机号不存在的异常。当用户不存在时,抛出了用户不存在的异常。

这就是所说的业务异常,使用DaMaiFrameException,而异常的信息是在BaseCode中保存,以后后续有新的业务信息,直接在BaseCode中添加即可

DaMaiFrameException中继承了BaseException,我们看下此类结构

BaseException

java
public class BaseException extends RuntimeException{
	
	public BaseException() {
		
	}
	
	public BaseException(String message) {
		super(message);
	}
	
	public BaseException(Throwable cause) {
		super(cause);
	}
	
	public BaseException(String message, Throwable cause) {
		super(message, cause);
	}

	public BaseException(Integer code, String message, Throwable cause) {
		super(message, cause);
	}
}

BaseException中继承了RuntimeException,这就和java的异常体系串联了起来,BaseException存在的意义是作为业务异常的基类,而DaMaiFrameException就是其中之一,如果后续有其他类型的业务异常,想更加的将异常细化,比如远程服务调用失败异常ServiceRpcException、Redis调用异常RedisExecption,只要集成BaseException即可

MethodArgumentNotValidException

看名字就能理解是验证方法入参的异常,比如参数的必填和格式限制等,验证这些功能有现成的工具可以使用,比如使用javax.validation来进行验证,使用起来很简单,首先,引入依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

接着在控制层的方法上,添加@Valid注解,示例:

java
@ApiOperation(value = "查询(通过手机号)")
@PostMapping(value = "/get/mobile")
public ApiResponse<UserVo> getByMobile(@Valid @RequestBody UserMobileDto userMobileDto){
    return ApiResponse.ok(userService.getByMobile(userMobileDto));
}

在入参的实体类上中,在相应的字段添加来进行验证规则的注解即可

java
@Data
@ApiModel(value="UserMobileDto", description ="用户手机号入参")
public class UserMobileDto {
    
    @ApiModelProperty(name ="name", dataType ="String", value ="用户手机号", required =true)
    @NotBlank
    private String mobile;
}

@NotBlank的作用是验证字符串不能为空,除了此注解外,工具中还提供了非常多的注解验证,比如NotNullNotEmptyEmail等,详细使用可参考源码注解即可

当不满于条件时,必须mobile的参数为空,那么就会触发抛出MethodArgumentNotValidException,则就会被com.damai.exception.DefaultExceptionHandler#validExceptionHandler感知到

Throwable

第三个异常是进行兜底的策略,当异常类型不属于DaMaiFrameException,也不属于MethodArgumentNotValidException,就会被com.damai.exception.DefaultExceptionHandler#defaultErrorHandler所捕获

通过以上就是将异常进行了统一处理,如果自己想额外捕获其他类型的异常,只需额外添加@ExceptionHandler(value = 定制的异常.class)

更新: 2026-01-09 17:50:59
原文: https://www.yuque.com/u22210564/ykdrdh/kmzz0slw67yent1b

Java 后端面试知识库