hibernate validator 校驗操作小結(jié)
@Override
public Result<List<Month>> getCalendarList(@Validated({ calendar.class }) @RequestBody GroupDTO dto)}...
在controller 的方法定義中加上 @Validated({ calendar.class }) 注解
(calendar.class 為校驗分組 ),那么按照該分組的規(guī)則校驗前端傳過來的值是否滿足定義的校驗規(guī)則绞蹦。
同一個dto可能會在多個handler的方法中使用,但是可能會要按照不同的校驗規(guī)則分組笛匙,如insert的controller需要這個對象的屬性都非空,但是刪除的controller只需要id非空即可。那么在dto定義的校驗規(guī)則就需要分組:
@NotNull(message="{ProductEmpty}",groups = {deleteGroup.class, addPriceList.class, addPriceWeeks.class,calendarPrice.class})
private Integer productId;
@NotNull(message="{MenuEmpty}",groups = {deleteGroup.class, addPriceList.class,addPriceWeeks.class,calendarPrice.class })
private Integer menuId;
如上所示在校驗注解中有g(shù)roups屬性可以定義這個校驗是屬于哪個分組的梆奈,而屬性的值只是一個空的接口垃喊,在controller方法的參數(shù)前使用@Validated({ calendar.class }) 即指定了這個方法使用哪組校驗吧恃,那么就只會校驗該dto下groups中有該組的屬性。
message中返回的是校驗失敗的信息 可以直接寫字符串如 “****不能為空” 也可以如上所示在定義一個properties文件統(tǒng)一每個返回信息翠霍。
可以看到這個文件名字中有zh_CN锭吨,這個其實是jar包中自帶的中文返回信息配置,我們同樣可以在hibernate-validator的jar包下找到英文的配置文件 配置它 這樣方便國際化寒匙。
DateEmpty=\u65E5\u671F\u4FE1\u606F\u5FC5\u4F20
ProductEmpty=\u4EA7\u54C1\u4FE1\u606F\u4E0D\u5168
具體配置中是這樣的 配置信息被轉(zhuǎn)碼
配置的好處是有一個地方統(tǒng)一管理了返回信息零如,方便修改。
校驗規(guī)則制定
1 普通校驗
把該注解的屬性填好 加到被校驗對象的字段上就可以了 注意一定要類型 匹配 如@MIN加到string頭上 運行就會報錯
2 復(fù)合校驗
如果說 我的對象里有對象屬性 锄弱,對象的屬性里又有對象 那么對這種深層次的校驗應(yīng)該這么做:
@Override
public Result<List<Month>> getCalendarList(@Validated({ calendar.class }) @RequestBody GroupDTO dto
) {....
方法定義這里不用變
dto校驗規(guī)則中
@NotEmpty(message="價格信息必傳",groups = { addPriceList.class,addPriceWeeks.class })
private List<@Valid PriceInfo> priceInfos;
首先在校驗的屬性對象前加上@validate注解
然后定義這個對象的校驗規(guī)則考蕾,即對priceInfo中各個屬性加上校驗注解當然想要在這里被校驗的字段的校驗分組要和外面一直為addPriceList.class 或另一個。
3 自定義校驗
如果說框架提供給我的校驗不能滿足我的場景需求 那么我們可以自定義校驗
屬性單獨校驗
如果我們要定義一個對單獨屬性的校驗類型:
package admtic.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import admtic.validator.IsNumValidator;
@Documented
@Constraint(validatedBy = IsNumValidator.class)
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface IsNum {
/*
* 用于驗證的注解下列這三個方法必須要会宪,這是Hibernate Validation框架要求的肖卧,否則程序再在調(diào)用的時候會報錯
* default用于對屬性給定默認值
* 如果不給定默認值,則在使用注解的時候必須給屬性指定屬性值掸鹅,否則報錯
* 給定默認值時塞帐,在使用注解的時候可以不用指定屬性值
*/
String message() default "不是數(shù)字!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 非必須屬性
// // 沒加default給定默認值巍沙,使用注解的時候該屬性必須賦值葵姥,否則報錯
// String regex();
// // value屬性,加上了default "mercy" 使得該屬性在使用注解的時候可以不用輸入也不會報錯
// String value() default "mercy";
}
先自定義一個校驗注解 然后再實現(xiàn)一個校驗器
package admtic.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import admtic.annotation.IsNum;
/**
* 泛型中一個是枚舉類一個是校驗支持的數(shù)據(jù)類型
* @author spf
* @version 2019年5月8日 下午3:46:35
*
*/
public class IsNumValidator implements ConstraintValidator<IsNum, Integer> {
// private String regex;
/**
* 通過initialize()可以獲取注解里的屬性值
*/
// @Override
// public void initialize(IsNum constraintAnnotation) {
// ConstraintValidator.super.initialize(constraintAnnotation);
// regex = constraintAnnotation.regex();
// }
/**
* 強行return true hahahhah
*/
@Override
public boolean isValid(Integer s, ConstraintValidatorContext arg1) {
if(s == -1){
}
return false;
}
}
需要注意的是 該校驗器泛型定義中 第一個是校驗注解的名字 第二個是校驗屬性的類型 isvalid返回是否校驗成功句携。
屬性聯(lián)合校驗
如果我們要定義一個對對象的聯(lián)合校驗榔幸,比如說如果有一個flag屬性,如果flag屬性為特殊值的話那么這個對象就不用校驗其他屬性了矮嫉,或者說另一種場景前端輸入的密碼和重復(fù)密碼牡辽,我后端這里要校驗一下這兩個屬性的值是否相同,不相同就定義校驗失敗敞临,不能操作态辛。這種多個屬性之間聯(lián)合起來一起影響校驗結(jié)果的聯(lián)合校驗需要我們這樣定義:
由于校驗注解是自定義的 我們可以把它定義到類頭上 然后校驗器里獲取到這個類對象再做聯(lián)合校驗
如
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
String message() default "{constraints.fieldmatch}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// /**
// * Defines several <code>@FieldMatch</code> annotations on the same element
// *
// * @see FieldMatch
// */
// @Target({TYPE, ANNOTATION_TYPE})
// @Retention(RUNTIME)
// @Documented
// @interface List
// {
// FieldMatch[] value();
// }
//只能有string 類型的參數(shù) 獲取后再轉(zhuǎn)
String priceTypeId();
String costPrice();
String sellPrice();
String stock();
String flag();
}
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import admtic.annotation.FieldMatch;
//校驗注解 也可以用于dto整個類 注意這里的泛型 前面的時自定義的注解,后面那個時被校驗的字段類型 這個用于對象屬性的聯(lián)合校驗 所以用object
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
// 只能有string 類型的參數(shù)
private String priceTypeId;
private String costPrice;
private String sellPrice;
private String stock;
private String flag;
@Override
public void initialize(final FieldMatch constraintAnnotation) {
priceTypeId = constraintAnnotation.priceTypeId();
costPrice = constraintAnnotation.costPrice();
sellPrice = constraintAnnotation.sellPrice();
stock = constraintAnnotation.stock();
flag = constraintAnnotation.flag();
}
@Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
try {
List<Field> fields = Arrays.asList(value.getClass().getDeclaredFields());
Integer priceTypeIdValue = null;
Double sellPriceValue = null;
Double costPriceValue = null;
Integer stockValue = null;
Integer flagValue = null;
fields.forEach(e->e.setAccessible(true));
Map<String, List<Field>> group =
fields.stream().collect(Collectors.groupingBy(Field::getName));
priceTypeIdValue = (Integer) group.get(priceTypeId).get(0).get(value);
if(priceTypeIdValue == null || priceTypeIdValue < 1){
return false;
}
flagValue = (Integer) group.get(flag).get(0).get(value);
if(flagValue == -1){
return true;
}
sellPriceValue = (Double) group.get(sellPrice).get(0).get(value);
costPriceValue = (Double) group.get(costPrice).get(0).get(value);
stockValue=(Integer) group.get(stock).get(0).get(value);
// System.out.println("######");
// System.out.println(priceTypeIdValue);
// System.out.println(sellPriceValue);
// System.out.println(costPriceValue);
// System.out.println(stockValue);
// System.out.println(flagValue);
if(sellPriceValue == null || costPriceValue == null || stockValue == null || priceTypeIdValue == null){
return false;
}
} catch (final Exception ignore) {
ignore.printStackTrace();
return false;
}
return true;
}
}
在校驗器中反射獲取屬性值 并進行校驗處理
校驗結(jié)果的處理
立即返回和全部校驗
對一個對象校驗時挺尿,比如說我們要校驗10個字段奏黑,第3個字段沒有通過炊邦,此時我們可以選擇馬上返回前端失敗,也可以選擇把后面的字段全部校驗熟史,拿到所有字段的校驗結(jié)果后再返回馁害。
對于第一種快速失敗的配置如下:
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
或
//failFast: true 快速失敗返回模式,false 普通模式
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
或
//hibernate.validator.fail_fast: true 快速失敗返回模式蹂匹,false 普通模式
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
1 handler 單獨處理
在每個controller方法中可以獲取校驗失敗后的錯誤信息 進行處理 如:
@Override
public Result<Boolean> addMenuOne(HttpServletRequest request, @RequestBody @Validated MenuDTO dto,
BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
return Result.failed(error.getDefaultMessage());
}
}
2 全局處理
每個方法都加這個處理太麻煩了 我們可以把這個錯誤跑出來 再全局處理
方法:
@Override
public Result<List<Month>> getCalendarList(@Validated({ calendar.class }) @RequestBody GroupDTO dto
) {
// if (result.hasErrors()) {
// for (ObjectError error : result.getAllErrors()) {
// return Result.failed(error.getDefaultMessage());
// }
// }
List<Month> months = groupService.getDefaultCalendar(dto);
return Result.success(months);
}
我們把bindingResult 去掉不做處理 之后 全局定義一個錯誤處理器:
package org.dmc.b.admtic.config;
import java.util.stream.Collectors;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.dmc.common.utils.Result;
@ControllerAdvice
public class GloableExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GloableExceptionHandler.class);
@ExceptionHandler({ ConstraintViolationException.class,
MethodArgumentNotValidException.class,
ServletRequestBindingException.class,
BindException.class })
@ResponseBody
public Result<?> handleValidationException(Exception e) {
String msg = "";
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException t = (MethodArgumentNotValidException) e;
msg = t.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(","));
} else if (e instanceof BindException) {
BindException t = (BindException) e;
msg = t.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(","));
} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException t = (ConstraintViolationException) e;
msg = t.getConstraintViolations().stream().map(ConstraintViolation::getMessage)
.collect(Collectors.joining(","));
} else if (e instanceof MissingServletRequestParameterException) {
MissingServletRequestParameterException t = (MissingServletRequestParameterException) e;
msg = t.getParameterName() + " 不能為空";
} else if (e instanceof MissingPathVariableException) {
MissingPathVariableException t = (MissingPathVariableException) e;
msg = t.getVariableName() + " 不能為空";
} else {
msg = "必填參數(shù)缺失";
}
log.warn("=========================**********=====================參數(shù)校驗不通過,msg: {}", msg);
return Result.failed(msg);
}
}
可以達到同樣的效果 但是又少些了很多代碼
返回前端
用到的注解
@ControllerAdvice
@ControllerAdvice 注解碘菜,spring3.2提供的新注解,控制器增強限寞,全局增強可以用于定義@ExceptionHandler忍啸、@InitBinder、@ModelAttribute履植,并應(yīng)用到所有@RequestMapping中
使用 @ControllerAdvice计雌,不用任何的配置,只要把這個類放在項目中玫霎,Spring能掃描到的地方凿滤。就可以實現(xiàn)全局異常的回調(diào)。
該注解使用@Component注解庶近,這樣的話當我們使用<context:component-scan>掃描時也能掃描到翁脆。
僅僅從命名上來看 advice就是通知,猜想它就是依賴一個aop實現(xiàn)鼻种。
這個后面有時間探究反番。
如:
@ControllerAdvice
public class MyControllerAdvice {
/**
* 應(yīng)用到所有@RequestMapping注解方法,在其執(zhí)行之前初始化數(shù)據(jù)綁定器
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
//可以對日期的統(tǒng)一處理
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
//也可以添加對數(shù)據(jù)的校驗
//binder.setValidator();
}
/**
* 把值綁定到Model中普舆,使全局@RequestMapping可以獲取到該值
* @param model
*/
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("author", "Magical Sam");
}
/**
* 全局異常捕捉處理
* @param ex
* @return
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Map errorHandler(Exception ex) {
Map map = new HashMap();
map.put("code", 100);
map.put("msg", ex.getMessage());
return map;
}
}
啟動應(yīng)用后掌逛,上述三個方法都會作用在 被 @RequestMapping 注解的方法上
@controllerAdvice源碼
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
我們可以傳遞basePackage,聲明的類(是一個數(shù)組)指定的Annotation參數(shù)
@ControllerAdvice是一個@Component寺晌,用于定義@ExceptionHandler账月,@InitBinder和@ModelAttribute方法蟆沫,適用于所有使用@RequestMapping方法。
Spring4之前蛾洛,@ControllerAdvice在同一調(diào)度的Servlet中協(xié)助所有控制器养铸。Spring4已經(jīng)改變:@ControllerAdvice支持配置控制器的子集,而默認的行為仍然可以利用轧膘。
在Spring4中钞螟, @ControllerAdvice通過annotations(), basePackageClasses(), basePackages() 方法定制用于選擇控制器子集。
ControllerAdvice定義的Class是有作用范圍的谎碍,默認情況下鳞滨,什么參數(shù)都不指定時它的作用范圍是所有的范圍。ControllerAdvice提供了一些可以縮小它的處理范圍的參數(shù)蟆淀。
value:數(shù)組類型拯啦,用來指定可以作用的基包澡匪,即將對指定的包下面的Controller及其子包下面的Controller起作用。
basePackages:數(shù)組類型褒链,等價于value唁情。
basePackageClasses:數(shù)組類型,此時的基包將以指定的Class所在的包為準甫匹。
assignableTypes:數(shù)組類型甸鸟,用來指定具體的Controller類型,它可以是一個共同的接口或父類等兵迅。
annotations:數(shù)組類型抢韭,用來指定Class上擁有指定的注解的Controller。
下面的ControllerAdvice將對定義在com.elim.app.mvc.controller包及其子包中的Controller起作用喷兼。
@ControllerAdvice(value="com.elim.app.mvc.controller")
下面的ControllerAdvice也將對定義在com.elim.app.mvc.controller包及其子包中的Controller起作用篮绰。
@ControllerAdvice(basePackages="com.elim.app.mvc.controller")
下面的ControllerAdvice也將對定義在com.elim.app.mvc.controller包及其子包中的Controller起作用后雷。它通過basePackageClasses指定了需要作為基包的Class季惯,此時基包將以basePackageClasses指定的Class所在的包為準,即com.elim.app.mvc.controller臀突。
@ControllerAdvice(basePackageClasses=com.elim.app.mvc.controller.Package.class)
面的ControllerAdvice將對FooController及其子類型的Controller起作用勉抓。
@ControllerAdvice(assignableTypes=FooController.class)
下面的ControllerAdvice將對所有Class上使用了RestController注解標注的Controller起作用。
@ControllerAdvice(annotations=RestController.class)
也可以同時指定多個屬性候学,比如下面的ControllerAdvice將對FooController及其子類型的Controller起作用藕筋,同時也將對com.elim.app.mvc.controller包及其子包下面的Controller起作用。
@ControllerAdvice(assignableTypes=FooController.class, basePackages="com.elim.app.m
@ExceptionHandler
@ExceptionHandler 攔截了異常梳码,我們可以通過該注解實現(xiàn)自定義異常處理隐圾。其中,@ExceptionHandler 配置的 value 指定需要攔截的異常類型掰茶。
需要注意的是使用@ExceptionHandler注解傳入的參數(shù)可以一個數(shù)組暇藏,且使用該注解時,傳入的參數(shù)不能相同濒蒋,也就是不能使用兩個@ExceptionHandler去處理同一個異常盐碱。如果傳入?yún)?shù)相同,則初始化ExceptionHandler時會失敗
當一個Controller中有方法加了@ExceptionHandler之后沪伙,這個Controller其他方法中沒有捕獲的異常就會以參數(shù)的形式傳入加了@ExceptionHandler注解的那個方法中
除了全局增強捕獲所有Controller中拋出的異常瓮顽,我們也可以使用接口的默認方法(1.8)
public interface DataExceptionSolver {
@ExceptionHandler
@ResponseBody
default Object exceptionHandler(Exception e){
try {
throw e;
} catch (SystemException systemException) {
systemException.printStackTrace();
return WebResult.buildResult().status(systemException.getCode())
.msg(systemException.getMessage());
} catch (Exception e1){
e1.printStackTrace();
return WebResult.buildResult().status(Config.FAIL)
.msg("系統(tǒng)錯誤");
}
}
}
但這種方法即依賴1.8 又需要controller實現(xiàn)接口 不如全局爽。
還有更加偷雞的方法時直接定義到handler中 @ExceptionHandler這個只會是在當前的Controller里面起作用
當一個Controller中有多個@ExceptionHandler注解出現(xiàn)時围橡,那么異常被哪個方法捕捉呢暖混?這就存在一個優(yōu)先級的問題,@ExceptionHandler的優(yōu)先級是:在異常的體系結(jié)構(gòu)中翁授,哪個異常與目標方法拋出的異常血緣關(guān)系越緊密拣播,就會被哪個捕捉到
@Controller
@RequestMapping(value = "exception")
public class ExceptionHandlerController {
@ExceptionHandler({ ArithmeticException.class })
public String handleArithmeticException(Exception e) {
e.printStackTrace();
return "error";
}
@RequestMapping(value = "e/{id}", method = {RequestMethod.GET })
@ResponseBody
public String testExceptionHandle(@PathVariable(value = "id") Integer id) {
System.out.println(10 / id);
return id.toString();
}
}
當訪問exception/e/0的時候善绎,會拋出ArithmeticException異常,@ExceptionHandler就會處理并響應(yīng)error.jsp
@ResponseStatus
這里還有一個注解@ResponseStatus 可以將某種異常映射為HTTP狀態(tài)碼
如:
@Controller
@RequestMapping(value = "status")
public class ResponseStatusController {
/**
* ResponseStatus修飾目標方法诫尽,無論它執(zhí)行方法過程中有沒有異常產(chǎn)生禀酱,用戶都會得到異常的界面。而目標方法正常執(zhí)行
* @param id
* @return
*/
@RequestMapping(value = "e2/{id}", method = { RequestMethod.GET })
@ResponseStatus(value = HttpStatus.BAD_GATEWAY)
@ResponseBody
public String status2(@PathVariable(value = "id") Integer id) {
System.out.println(10 / id);
return id.toString();
}
}
這樣前端即使請求成功了也會返回 502的狀態(tài)碼
那么 將這個注解也放入controller增強的代碼中
/**
* 捕獲CustomException
* @param e
* @return json格式類型
*/
@ResponseBody
@ExceptionHandler({CustomException.class}) //指定攔截異常的類型
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //自定義瀏覽器返回狀態(tài)碼
public Map<String, Object> customExceptionHandler(CustomException e) {
Map<String, Object> map = new HashMap<>();
map.put("code", e.getCode());
map.put("msg", e.getMsg());
return map;
}
/**
* 捕獲CustomException
* @param e
* @return 視圖
*/
// @ExceptionHandler({CustomException.class})
// public ModelAndView customModelAndViewExceptionHandler(CustomException e) {
// Map<String, Object> map = new HashMap<>();
// map.put("code", e.getCode());
// map.put("msg", e.getMsg());
// ModelAndView modelAndView = new ModelAndView();
// modelAndView.setViewName("error");
// modelAndView.addObject(map);
// return modelAndView;
// }
那么單獨多定義幾個處理方法 即可控制返回的錯誤碼