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