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注解