Loading

SpringBoot校验(validation)+全局处理异常

SpringBoot校验(validation)+全局处理异常

SpringBoot校验(validation)

加入依赖hibernate-validator
springBoot中可以直接引用starter

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

这里面正是包含了我们真正需要的hibernate-validator依赖

<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.2.0.Final</version>
  <scope>compile</scope>
</dependency>

hibernate-validator中有很多非常简单好用的校验注解,例如NotNull,@NotEmpty,@Min,@Max,@Email,@PositiveOrZero等等。这些注解能解决我们大部分的数据校验问题。如下所示:

package com.nobody.dto;

import lombok.Data;

import javax.validation.constraints.*;

@Data
public class UserDTO {

    @NotBlank(message = "姓名不能为空")
    private String name;

    @Min(value = 18, message = "年龄不能小于18")
    private int age;

    @NotEmpty(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
}

控制层代码入参处记得加上@Valid注解开启校验,不然不生效。

@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("add")
    public UserDTO add(@RequestBody @Valid UserDTO userDTO) {
        System.out.println(">>> 用户开户成功...");
        return userDTO;
    }

}

二 自定义参数校验器

但是,hibernate-validator中的这些注解不一定能满足我们全部的需求,我们想校验的逻辑比这复杂,那么我们可以自定义自己的参数校验注解。

像它提供的注解一样我们可以自定义自己的注解。
在这里插入图片描述

自定义注解类

package com.zry.seckill.validator;

import com.zry.seckill.vo.IsMobileValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author zry
 * @ClassName IsMobile.java
 * @Description TODO
 * @createTime 2021年10月20日
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})//指定我们的校验类,这里面实现了我们需要的具体校验方法
public @interface IsMobile {
   /**
     * 是否强制校验
     * 
     * @return 是否强制校验的boolean值
     */
    boolean required() default true;  
    
  /**
     * 校验不通过时的报错信息
     * 
     * @return 校验不通过时的报错信息
     */
    String message() default "手机号码格式错误";
    
   /**
     * 将validator进行分类,不同的类group中会执行不同的validator操作
     * 
     * @return validator的分类类型
     */
    Class<?>[] groups() default {};
    
   /**
     * 主要是针对bean,很少使用
     * 
     * @return 负载
     */
    Class<? extends Payload>[] payload() default {};

}

接下来我们完成 定义校验类,实现ConstraintValidator接口,接口使用了泛型,需要指定两个参数,
第一个是自定义注解,第二个是需要校验的数据类型。
重写2个方法,initialize方法主要做一些初始化操作,它的参数是我们使用到的注解,可以获取到运行时的注解信息。
isValid方法就是要实现的校验逻辑,被注解的对象会传入此方法中。

package com.zry.seckill.vo;

import com.zry.seckill.utils.ValidatorUtil;
import com.zry.seckill.validator.IsMobile;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**手机校验规则
 * @author zry
 * @ClassName IsMobileValidator.java
 * @Description 手机校验规则
 * @createTime 2021年10月20日
 */
public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {

    private boolean required = false;// 是否强制校验
    @Override
    public void initialize(IsMobile constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
		//我这里调用了自定义的工具类
        return ValidatorUtil.isMobile(value);  // 返回boolean型,true代表通过校验
    }
}

最后别忘了在Controller层加上 @Valid 注解才能启用注解校验。
在这里插入图片描述
当然还有另一种@Validated,和@Valid有所不同,这里不做详述。

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring’s
JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。

通过以上几个步骤,我们自定义的校验注解就完成了。

如果参数校验不通过,会抛出MethodArgumentNotValidException异常,我们全局处理下然后返回给接口。

SpringBoot全局处理异常

为什么要处理异常?

在日常开发中,为了不抛出异常堆栈信息给前端页面,每次编写Controller层代码都要尽可能的catch住所有service层、dao层等异常,代码耦合性较高,且不美观,不利于后期维护。
为解决该问题,我们将Controller层异常信息统一封装处理,且能区分对待Controller层方法返回给前端的String、Map、JSONObject、ModelAndView等结果类型。

简单介绍

使用注解:

  • @ControllerAdvice+@ResponseBody + @ExceptionHandler

对于@ControllerAdvice,我们比较熟知的用法是结合@ExceptionHandler用于全局异常的处理,但其作用不止于此。ControllerAdvice拆开来就是Controller
Advice,关于Advice,在Spring的AOP中,是用来封装一个切面所有属性的,包括切入点和需要织入的切面逻辑。这里ControllerAdvice也可以这么理解,其抽象级别应该是用于对Controller进行切面环绕的,而具体的业务织入方式则是通过结合其他的注解来实现的。@ControllerAdvice是在类上声明的注解,其用法主要有三点:

1.结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的。

2.结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的。

3.结合方法型注解@ModelAttribute,表示其注解的方法将会在目标Controller方法执行之前执行。

我们这次使用@RestControllerAdvice + @ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的。

方案一

这个是直接建一个异常处理类直接继承 Exception
在这里插入图片描述

异常处理类

package com.zry.simpleblog.handler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

/**
 * 使用 @ControllerAdvice 注释来管理应用程序中的异常。
 * @ControllerAdvice是@Component 注解的一种特殊化,它允许在一个全局处理组件中处理整个应用程序中的异常。
 * 它可以被视为一个拦截器,拦截由注释@RequestMapping和类似的方法抛出的异常。
 * @author zry
 * @ClassName ControllerExceptionHandler.java
 * @Description TODO
 * @createTime 2021年08月28日
 */
@RestControllerAdvice
public class ControllerExceptionHandler extends Exception {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @ExceptionHandler(value = {Exception.class})//指定捕获异常的类型,这里是全部捕获
    
    public ModelAndView exceptionHandler(HttpServletRequest request, Exception e) throws Exception {
        ModelAndView mv = new ModelAndView();
        logger.error("Request URl : {}, Exception : {}",request.getRequestURI(),e);

//        如果定制了http状态码那么抛出异常
        if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
            throw e;
        }

        mv.addObject("url", request.getRequestURI());
        mv.addObject("exception",e);
        mv.setViewName("error/error");
        return mv;
    }
}

这样一个简单全局处理就做好了。

方案二

我们先建一个excepttion包来放我们的全局异常类。
在这里插入图片描述

自定义异常类

package com.zry.seckill.exception;

import com.zry.seckill.vo.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author zry
 * @ClassName GlobalException.java
 * @Description TODO
 * @createTime 2022年01月01日
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException{
    private RespBeanEnum respBeanEnum;
}

自定义全局异常处理

package com.zry.seckill.exception;

import com.zry.seckill.vo.RespBean;
import com.zry.seckill.vo.RespBeanEnum;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


/**
 * @author zry
 * @ClassName GlobalExceptionHandler.java
 * @Description TODO
 * @createTime 2022年01月01日
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    //调试日志
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @ExceptionHandler(Exception.class)//处理哪些异常
    public RespBean ExceptionHandler(Exception e,HttpServletRequest request){
    //打印日志
    logger.error("Requst URL : {},Exception : {}", request.getRequestURL(),e);
    
        if(e instanceof GlobalException){//如果是咱们之前自定义的异常
            GlobalException ex = (GlobalException) e;
            return RespBean.error(ex.getRespBeanEnum());
        }else if(e instanceof BindException){ //如果异常是绑定异常(比如没有通过参数校验注解抛出的异常)
            BindException be = (BindException) e;
            RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
            respBean.setMessage("参数校验异常" + be.getBindingResult().getAllErrors().get(0).getDefaultMessage());
            return respBean;
        }
        return RespBean.error(RespBeanEnum.ERROR);
    
    }
	
}

这里我们就已经实现了对Controller层的全局异常处理。

下面是对上面方法中出现RespBeanEnum和RespBean的一些说明

GlobalExceptionHandler() 类中都是我们自己编写的处理异常的方法。
我这里的ExceptionHandler()方法只是个例子。
其中RespBeanEnum是我自定义的枚举类。它主要是为RespBean类服务。RespBean相当于统一返回实体类
用来返回结果信息。

返回类型枚举

package com.zry.seckill.vo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

/**
 * @author zry
 * @ClassName RespBeanEnum.java
 * @Description 公共返回对象枚举
 * @createTime 2021年10月15日
 */
@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
    SUCCESS(200,"SUCCESS"),
    ERROR(500,"服务端异常"),
    LOGIN_ERROR(500210,"用户名或密码错误"),
    MOBILE_ERROR(500211,"手机号码格式不正确"),
    BIND_ERROR(500212,"参数校验异常");

    private final Integer code;
    private final String message;


}

返回结果类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
    private long code;
    private String message;
    private Object obj;

    /**
     * 功能描述:返回成功结果
     * @param
     * @return
     */
    public static RespBean success(){
        return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);
    }

    /**
     * 功能描述:返回成功结果
     * @param obj
     * @return
     */
    public static RespBean success(Object obj){
        return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),obj);
    }
    /**
     * 功能描述:返回失败结果
     * @param respBeanEnum
     * @return
     */
    public static RespBean error(RespBeanEnum respBeanEnum){
        return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
    }
    /**
     * 功能描述:返回失败结果
     * @param respBeanEnum,obj
     * @return
     */
    public static RespBean error(RespBeanEnum respBeanEnum,Object obj){
        return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
    }
}

@RestControllerAdvice与@ControllerAdvice的区别

@RestControllerAdvice与@ControllerAdvice的区别就和@RestController与@Controller的区别类似,@RestControllerAdvice注解包含了@ControllerAdvice注解和@ResponseBody注解。
(参考https://www.pianshen.com/article/6221932106/)
@RestControllerAdvice源码中有@ControllerAdvice注解和@ResponseBody注解,当自定义类加@RestControllerAdvice注解时,方法自动返回json数据,每个方法无需再添加@ResponseBody注解

posted @ 2022-01-31 17:24  程序员小小宇  阅读(4382)  评论(0编辑  收藏  举报