在 Spring Boot 項目中,?自定義異常通常用于處理特定的業務邏輯錯誤,并結合全局異常處理器(@ControllerAdvice
)統一返回結構化的錯誤信息。
一.全局異常處理器:?
1. 自定義異常類?
定義一個繼承自?RuntimeException
?的業務異常類,包含錯誤碼和錯誤信息:
public class BusinessException extends RuntimeException {private final int code; // 自定義錯誤碼private final String message; // 錯誤信息public BusinessException(int code, String message) {super(message);this.code = code;this.message = message;}// 使用枚舉定義錯誤類型(推薦)public BusinessException(ErrorCode errorCode) {super(errorCode.getMessage());this.code = errorCode.getCode();this.message = errorCode.getMessage();}// Getterpublic int getCode() { return code; }@Override public String getMessage() { return message; }
}
?配套的枚舉:
public enum ErrorCode {USER_NOT_FOUND(1001, "用戶不存在"),INVALID_PARAM(1002, "參數無效"),INTERNAL_ERROR(5000, "系統內部錯誤");private final int code;private final String message;ErrorCode(int code, String message) {this.code = code;this.message = message;}// Getterpublic int getCode() { return code; }public String getMessage() { return message; }
}
2. 全局異常處理器?
使用?@ControllerAdvice
?捕獲異常并統一返回 JSON 格式的錯誤響應:
@Slf4j
@ControllerAdvice // 標記該類為全局異常處理器,可以攔截所有Controller拋出的異常
@ResponseBody // 或直接使用 @RestControllerAdvice
public class GlobalExceptionHandler {/?**?* 處理業務異常*/@ExceptionHandler(BusinessException.class) // 處理自定義異常BusinessException@ResponseStatus(HttpStatus.BAD_REQUEST) // 指定HTTP響應的狀態碼 400 錯誤public Result<Void> handleBusinessException(BusinessException e) {log.error("業務異常: code={}, message={}", e.getCode(), e.getMessage());return Result.fail(e.getCode(), e.getMessage());}/?**?* 處理系統異常(兜底)*/@ExceptionHandler(Exception.class) // 處理所有未被其他處理器捕獲的異常@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 指定HTTP響應的狀態碼 500 錯誤public Result<Void> handleException(Exception e) {log.error("系統異常: ", e);return Result.fail(ErrorCode.INTERNAL_ERROR.getCode(), "系統繁忙,請稍后重試");}
}
統一的響應封裝類:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {private int code;private String message;private T data;// 成功響應public static <T> Result<T> success(T data) {return new Result<>(200, "成功", data);}// 失敗響應public static <T> Result<T> fail(int code, String message) {return new Result<>(code, message, null);}
}
3. 在業務代碼中拋出異常:
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/{id}")public Result<User> getUser(@PathVariable Long id) {User user = userService.findById(id);if (user == null) {throw new BusinessException(ErrorCode.USER_NOT_FOUND); // 拋出業務異常}return Result.success(user);}
}
4.使用方法:
throw new BusinessException(1001, "用戶不存在");
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
二.對于JSR303校驗的異常處理:
早在JavaEE6規范中就定義了參數校驗的規范,它就是JSR-303,它定義了Bean Validation,即對bean屬性進行校驗。
SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底層使用Hibernate Validator,Hibernate Validator是Bean Validation 的參考實現。
所以,我們準備在Controller層使用spring-boot-starter-validation完成對請求參數的基本合法性進行校驗。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在javax.validation.constraints包下有很多這樣的校驗注解,直接使用注解定義校驗規則即可。
例子如下:
package com.xuecheng.content.model.dto;import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.ToString;/*** @description 添加課程dto* @author Mr.M* @date 2022/9/7 17:40* @version 1.0*/
@Data
@ToString
@Schema(name = "AddCourseDto", description = "新增課程基本信息")
public class AddCourseDto {@NotEmpty(message = "課程名稱不能為空")@Schema(description = "課程名稱", required = true)private String name;@NotEmpty(message = "適用人群不能為空")@Size(message = "適用人群內容過少",min = 10)@Schema(description = "適用人群", required = true)private String users;@NotEmpty(message = "課程分類不能為空")@Schema(description = "大分類", required = true)private String mt;@NotEmpty(message = "課程分類不能為空")@Schema(description = "小分類", required = true)private String st;@NotEmpty(message = "課程等級不能為空")@Schema(description = "課程等級", required = true)private String grade;@Schema(description = "教學模式(普通,錄播,直播等)", required = true)private String teachmode;@Schema(description = "課程介紹")private String description;@Schema(description = "課程圖片", required = true)private String pic;@NotEmpty(message = "收費規則不能為空")@Schema(description = "收費規則,對應數據字典", required = true)private String charge;
}
?上邊用到了@NotEmpty和@Size兩個注解,@NotEmpty表示屬性不能為空,@Size表示限制屬性內容的長短。
定義好校驗規則還需要開啟校驗,在controller方法中添加@Validated注解,如下:
@Operation("新增課程基礎信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated AddCourseDto addCourseDto){//機構id,由于認證系統沒有上線暫時硬編碼Long companyId = 1L;return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}
如果校驗出錯Spring會拋出MethodArgumentNotValidException異常,我們需要在統一異常處理器中捕獲異常,解析出異常信息。
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {// 拿到校驗框架BindingResult bindingResult = e.getBindingResult();List<String> msgList = new ArrayList<>();// 將錯誤信息放在msgListbindingResult.getFieldErrors().stream().forEach(item->msgList.add(item.getDefaultMessage()));// 將msgList的錯誤拼接String msg = StringUtils.join(msgList, ",");log.error("【系統異常】{}",msg);return new RestErrorResponse(msg);
}
有時候在同一個屬性上設置一個校驗規則不能滿足要求,比如:訂單編號由系統生成,在添加訂單時要求訂單編號為空,在更新訂單時要求訂單編寫不能為空。
此時就用到了分組校驗,同一個屬性定義多個校驗規則屬于不同的分組,比如:添加訂單定義@NULL 規則屬于 insert 分組,更新訂單定義@NotEmpty規則屬于 update 分組,insert 和update 是分組的名稱,是可以修改的。
下邊舉例說明,我們用class類型來表示不同的分組,所以我們定義不同的接口類型(空接口)表示不同的分組,由于校驗分組是公用的,所以定義在 base工程中。如下:
package com.xuecheng.base.execption;/*** @description 校驗分組* @version 1.0*/
public class ValidationGroups {public interface Inster{};public interface Update{};public interface Delete{};}
下邊在定義校驗規則時指定分組:
@NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加課程名稱不能為空")
@NotEmpty(groups = {ValidationGroups.Update.class},message = "修改課程名稱不能為空")
// @NotEmpty(message = "課程名稱不能為空")
@ApiModelProperty(value = "課程名稱", required = true)
private String name;
在Controller方法中啟動校驗規則指定要使用的分組名:
@Operation("新增課程基礎信息")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto){//機構id,由于認證系統沒有上線暫時硬編碼Long companyId = 1L;return courseBaseInfoService.createCourseBase(companyId,addCourseDto);
}
再次測試,由于這里指定了Insert分組,所以拋出 異常信息:添加課程名稱不能為空。
如果修改分組為ValidationGroups.Update.class,異常信息為:修改課程名稱不能為空。