3. 内容模块管理 - 异常处理与校验

内容模块管理

一、自定义异常

之前也学过对异常的统一管理,既然现在又看到了,就再学学springboot——全局异常处理器及封装结果集

如果一致对异常进行try…catch…,代码也会很冗余,我们直接throw就行,然后可以对异常进行统一处理

不论是那一层,都会有异常的出现,我们遇到异常都是向上抛出,最后让框架对异常统一处理

假如dao出异常就会抛给service,service有异常就会抛给controller,controller会抛给spring框架

image-20231115212256176

1.1 全局异常处理器

  • @ExceptionHandler

    提供的标识在方法上或类上的注解,用来表明方法的处理异常类型

  • @ControllerAdvice

    控制器增强,在项目中来增强SpringMVC中的Controller。通常和@ExceptionHandler结合使用,来处理SpringMVC的异常信息。

  • @ResponseStatus

    提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。

    调用处理程序方法时,状态代码将应用于HTTP响应

/**
 * 统一异常处理类
 */
@Slf4j
@ControllerAdvice
@ResponseBody//返回JSON数据
//@RestControllerAdvice = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
    
    

    /**
     * 针对自定义异常进行处理的
     */
    @ResponseBody
    @ExceptionHandler(XueChengPlusException.class)//捕捉这个异常
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//状态码500
    public RestErrorResponse customException(XueChengPlusException e) {
    
    
//      捕捉到异常的信息后解析异常的信息
        log.error("【系统异常】{}", e.getErrMessage(), e);
        return new RestErrorResponse(e.getErrMessage());
    }

    /**
     * 捕捉除了自定义异常以外的其他异常
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//状态码500
    public RestErrorResponse exception(Exception e) {
    
    
        log.error("【系统异常】{}", e.getMessage(), e);
        return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());//执行过程异常,请重试

    }

}

1.2 自定义异常

@EqualsAndHashCode(callSuper = true)
@Data
public class XueChengPlusException extends RuntimeException {
    
    

   private static final long serialVersionUID = -378480393877627738L;

   private String errMessage;

    public XueChengPlusException() {
    
    
        super();
    }

    public XueChengPlusException(String errMessage) {
    
    
        super(errMessage);
        this.errMessage = errMessage;
    }


    public static void cast(CommonError commonError) {
    
    
        throw new XueChengPlusException(commonError.getErrMessage());
    }

    public static void cast(String errMessage) {
    
    
        throw new XueChengPlusException(errMessage);
    }

}

1.3 异常统一响应类

/**
 * 和前端约定返回的异常信息模型
 * 错误响应参数包装
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RestErrorResponse implements Serializable {
    
    

    private static final long serialVersionUID = 9026504397012666687L;

    private String errMessage;

}

1.4 封装通用异常信息

/**
 * 通用异常信息
 */
public enum CommonError {
    
    

   UNKOWN_ERROR("执行过程异常,请重试。"),
   PARAMS_ERROR("非法参数"),
   OBJECT_NULL("对象为空"),
   QUERY_NULL("查询结果为空"),
   REQUEST_NULL("请求参数为空");

   private String errMessage;

   public String getErrMessage() {
    
    
      return errMessage;
   }

   private CommonError( String errMessage) {
    
    
      this.errMessage = errMessage;
   }

}

二、JSR303校验

SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。

所以,我们准备在Controller层使用spring-boot-starter-validation完成对请求参数的基本合法性进行校验。

  • 前端请求后端接口传输参数,是在controller中校验还是在Service中校验

    答案是都需要校验,只是分工不同。因为某些Service不仅仅被一个Controller调用,如果Service中不写校验再被其他Controller或其他Service调用时也会发生问题

  • Controller里一般校验什么

    Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。

  • Service里一般校验什么

    Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。

    Service中根据业务规则去校验不方便写成通用代码,Controller中则可以将校验的代码写成通用代码

2.1 Maven坐标

首先在Base工程添加spring-boot-starter-validation的依赖

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

在javax.validation.constraints包下有很多这样的校验注解,直接使用注解定义校验规则即可

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.2 校验规则

image-20231115224419263

2.3 代码示例

现在准备对内容管理模块添加课程接口进行参数校验,如下接口

定义好校验规则还需要开启校验,在controller方法中添加@Validated注解,否则校验不会生效

    @ApiOperation("新增课程")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@Validated @RequestBody AddCourseDto addCourseDto) {
    
    
//      将来会集成SpringSecurity框架,用户登录之后就可以获取到用户所属机构的ID
//      先把机构ID写死
        return courseBaseInfoService.createCourseBase(10086L,addCourseDto);
    }

进入AddCourseDto类,在属性上添加校验规则

@NotEmpty表示属性不能为空

@Size表示限制属性内容的长短

/**
 * @description 添加课程dto
 */
@Data
@ApiModel(value = "AddCourseDto", description = "新增课程基本信息")
public class AddCourseDto {
    
    

    @NotEmpty(message = "课程名称不能为空")
    @ApiModelProperty(value = "课程名称", required = true)
    private String name;

    @NotEmpty(message = "适用人群不能为空")
    @Size(message = "适用人群内容过少", min = 10)
    @ApiModelProperty(value = "适用人群", required = true)
    private String users;

    @ApiModelProperty(value = "课程标签")
    private String tags;

    @NotEmpty(message = "课程分类不能为空")
    @ApiModelProperty(value = "大分类", required = true)
    private String mt;

    @NotEmpty(message = "课程分类不能为空")
    @ApiModelProperty(value = "小分类", required = true)
    private String st;

    @NotEmpty(message = "课程等级不能为空")
    @ApiModelProperty(value = "课程等级", required = true)
    private String grade;

    @ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true)
    private String teachmode;

    @ApiModelProperty(value = "课程介绍")
    private String description;

    @ApiModelProperty(value = "课程图片", required = true)
    private String pic;

    @NotEmpty(message = "收费规则不能为空")
    @ApiModelProperty(value = "收费规则,对应数据字典", required = true)
    private String charge;

    @ApiModelProperty(value = "价格")
    private Float price;
    @ApiModelProperty(value = "原价")
    private Float originalPrice;


    @ApiModelProperty(value = "qq")
    private String qq;

    @ApiModelProperty(value = "微信")
    private String wechat;
    @ApiModelProperty(value = "电话")
    private String phone;

    @ApiModelProperty(value = "有效期")
    private Integer validDays;
}

2.4 捕捉校验异常

该异常表示在方法参数验证失败时抛出,其中验证失败的详细信息可以通过 BindingResult 对象获取

getBindingResult() 方法是 MethodArgumentNotValidException 类的一个方法,用于获取包含验证错误详细信息的 BindingResult 对象。BindingResult 包含了验证结果,包括错误对象、错误代码、默认错误消息等。

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RestErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
    
    
//      这个异常对象有一个方法getBindingResult
        BindingResult bindingResult = e.getBindingResult();
        List<String> msgList = new ArrayList<>();
        //将错误信息放在msgList
        //因为校验的时候可能多个字段都会出现问题,所以使用了一个list集合存放有问题的消息,我们后面会将他们一块拼接起来的
        bindingResult.getFieldErrors().stream().forEach(item -> msgList.add(item.getDefaultMessage()));
        //拼接错误信息
        String msg = StringUtils.join(msgList, ",");
        log.error("【系统异常】{}", msg);
        return new RestErrorResponse(msg);
    }

比如下面的这一次请求中就是两个字段不合格,都会放到List集合中,最后由StringUtils.join将其拼接起来

image-20231115231654132

2.5 分组校验

假如修改课程的Dto和增加课程的Dto是一个样子的,但是校验规则不一样,怎么办呢

方法1:定义两个Dto来分别校验

方法2:分组校验

分组校验:将Dto中的校验规则分组,新增课程使用一个检验规则组,修改课程使用一个校验规则组,但是都还是同一个Dto

定义分组

/**
 * 用于分组校验,定义一些常用的组
 */
public class ValidationGroups {
    
    

 public interface Insert{
    
    };
 public interface Update{
    
    };
 public interface Delete{
    
    };

}

划分组别

//    @NotEmpty(message = "课程名称不能为空")
//  这个地方就说明了下面@NotEmpty校验属于ValidationGroups.Insert.class组别
    @NotEmpty(message = "新增课程名称不能为空",groups = {
    
    ValidationGroups.Insert.class})
//  这个地方就说明了下面@NotEmpty校验属于ValidationGroups.Update.class组别
    @NotEmpty(message = "修改课程名称不能为空",groups = {
    
    ValidationGroups.Update.class})
    @ApiModelProperty(value = "课程名称", required = true)
    private String name;

此时Controller中也需要修改

    @ApiOperation("新增课程")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@Validated(ValidationGroups.Insert.class) @RequestBody AddCourseDto addCourseDto) {
    
    
//      将来会集成SpringSecurity框架,用户登录之后就可以获取到用户所属机构的ID
//      先把机构ID写死
        return courseBaseInfoService.createCourseBase(10086L,addCourseDto);
    }

image-20231115232957202

假如说是修改的话,如下所示

 public CourseBaseInfoDto  updateCourseBase(@Validated(ValidationGroups.Update.class) @RequestBody AddCourseDto addCourseDto) {
    
    
    ...
}

2.6 备注

如果javax.validation.constraints包下的校验规则满足不了需求怎么办?

1、手写校验代码 。

2、自定义校验规则注解。

三、全局异常处理2

上面的异常处理是在看学成在线的时候做的笔记,但是怎么看怎么别扭,特别是封装的响应结果集,很别扭,然后就再整理一份

系统如何处理异常

  1. 我们自定义一个统一的异常处理器去捕获并处理异常

  2. 使用控制器增加注解@ControllerAdvice和异常处理注解@ExceptionHandler来实现

  3. 处理自定义异常

    程序在编写代码时根据校验结果主动抛出自定义异常类对象,抛出异常时指定详细的异常信息,异常处理器捕获异常信息记录异常日志并响应给用户

  4. 处理自定义异常

    接口执行过程中的一些运行时异常也会由异常处理器统一捕获,记录异常日志,统一响应给用户500错误(状态码由前后端协定)

  5. 在异常处理中还可以针对某个异常类型进行单独处理

3.1 全局异常处理器

/**
 * 全局异常处理
 * 底层是通过代理,代理controller,通过AOP把我们的一些方法拦截到,如果有异常,就在这个类统一进行处理
 * 下面就是只要带有RestController.class, Controller.class,Service.class的注解的类或方法出现异常,我们都会进行统一处理
 */
@ControllerAdvice(annotations = {
    
    RestController.class, Controller.class, Service.class})  // 通知
@ResponseBody  //我们需要返回JSON数据
@Slf4j
public class GlobalExceptionHandler {
    
    
 
//  表示处理SQL异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
    
    
//      打印日志信息
        log.error(ex.getMessage());//Duplicate entry 'zhangjingqi' for key 'employee.idx_username'
 
//      在这里也可以判断异常的具体信息,比如:
        if(ex.getMessage().contains("Duplicate entry")){
    
    
            String[] s = ex.getMessage().split(" ");
            String username = s[2];
            String msg =s[2]+"已经存在";
           return   R.error(msg);
        }
//      其他情况下可以直接输出
        return R.error("未知错误:"+ex.getMessage());
    }
}
 

3.2 结果集

不论是成功响应还是异常响应,都使用下面这个结果集

/**
 * 通用返回结果类,服务端响应的数据都会封装成此对象
 * @param <T>  这个类会接受多种类型,可能是普通对象,可能是数组、集合等等等等,所以我们要将这个加个泛型<T>,表示可以接收任何参数
 */
//  为什么不用Object,而用<T>?  如果用object需要强转类型 而T不用
@Data
public class R<T> {
    
    
 
    private Integer code; //编码:1成功,0和其它数字为失败
 
    private String msg; //错误信息
 
    private T data; //数据
 
    private Map map = new HashMap(); //动态数据
 
//  方法的返回值及参数中的T属于泛型
    public static <T> R<T> success(T object) {
    
    
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;   //成功
        return r;
    }
 
    public static <T> R<T> error(String msg) {
    
    
        R r = new R();
        r.msg = msg;
        r.code = 0;   //失败
        return r;
    }
 
    public R<T> add(String key, Object value) {
    
    
        this.map.put(key, value);
        return this;
    }
 
}

3.3 常用注解

3.3.1 @ControllerAdvice

SpringMVC 中 @ControllerAdvice 注解的三种使用场景! - 江南一点雨 - 博客园 (cnblogs.com)

  • basePackages/basePackageClasses属性

    指定扫描哪个包

@ControllerAdvice(basePackages = "com.zhangjingqi.controller")
public class GlobalExceptionHandler {
    
    
    // ...
}

  • annotations属性

    指定注解类型,限定只有被特定注解标记的控制器才会受到 @ControllerAdvice 类的影响

@ControllerAdvice(annotations = {
    
    RestController.class, Controller.class, Service.class})  // 通知
@ResponseBody  //我们需要返回JSON数据
@Slf4j
public class GlobalExceptionHandler {
    
    
    .......
}
  • assignableTypes属性

    通过指定类类型,限定只有继承自特定类的控制器才会受到 @ControllerAdvice 类的影响

@ControllerAdvice(assignableTypes = MyController.class)
public class GlobalExceptionHandler {
    
    
    // ...
}

  • value

    basePackages 属性类似,用于指定扫描的包。在很多情况下,value 属性可以替代 basePackages

@ControllerAdvice(value = "com.zhangjingqi.controllers")
public class GlobalExceptionHandler {
    
    
    // ...
}

  • useDefaultResponseAdvice

    默认为 true。当设置为 false 时,禁用默认的 ResponseEntityExceptionHandler,这样你就可以完全掌控异常处理的行为

@ControllerAdvice(useDefaultResponseAdvice = false)
public class CustomExceptionHandler {
    
    
    // ...
}

3.3.2 @ExceptionHandle

可以捕获到controller中抛出的一些自定义异常,统一进行处理,一般用于进行一些特定的异常处理

可以根据需要定义多个 @ExceptionHandler 方法,每个方法处理一种特定类型的异常

//  表示处理SQL异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
    
    
        ......
    }
  • value

    指定要处理的异常类型。可以是单个异常类或一组异常类

@ExceptionHandler(value = {
    
    NullPointerException.class, IllegalArgumentException.class})
public String handleSpecificExceptions(Exception e) {
    
    
    // 处理特定类型的异常
    return "处理特定类型的异常";
}

  • exceptions

    value 属性类似,用于指定要处理的异常类型

@ExceptionHandler(exceptions = NullPointerException.class)
public String handleCustomException(Exception e) {
    
    
    // 处理自定义异常
    return "处理自定义异常";
}

  • basePackages/basePackageClasses

    限定 @ExceptionHandler 方法的扫描范围,类似于 @ControllerAdvice 的属性

@ExceptionHandler(basePackages = "com.zhangjingqi.controllers")
public String handleExceptionsInControllerPackage(Exception e) {
    
    
    // 处理指定包中的异常
    return "packageError";
}

猜你喜欢

转载自blog.csdn.net/weixin_51351637/article/details/135032158