阿粉寫了八千多字,只為講透參數合法性驗證
最近很多讀者給阿粉留言,說怎么好久沒看到我的文章了,這里說一下。
由于公眾號不再按時間線排序,所以你會發現有時候能看到幾天前的文章,這不是出BUG,是公眾號的一次改變。
至于排序的具體標準是啥,阿粉也不太清楚,大概和你打開某個公眾號的頻率有關。
所以如果你想第一時間收到阿粉的文章,可以點擊Java極客技術的的頭像,再點右上角三個點,進去設置一下【星標】。
一、介紹
關于參數合法性驗證的重要性就不多說了,即使前端對參數做了基本驗證以外,后端依然還需要進行驗證,以防不合規的數據直接進入后端,嚴重的甚至會造成系統直接崩潰!
本文結合自己在項目中的實際使用經驗,主要以實用為主,對數據合法性驗證做一次總結,不了解的朋友可以學習一下,同時可以立馬實踐到項目上去。
下面我們通過幾個示例來演示如何判斷參數是否合法,不多說直接開擼!
二、斷言驗證
對于參數的合法性驗證,最初的做法比較簡單,自定義一個異常類。
- public class CommonException extends RuntimeException {
- /**錯誤碼*/
- private Integer code;
- /**錯誤信息*/
- private String msg;
- //...set/get
- public CommonException(String msg) {
- super(msg);
- this.msg = msg;
- }
- public CommonException(String msg, Throwable cause) {
- super(msg, cause);
- this.msg = msg;
- }
- }
當判斷某個參數不合法的時候,直接拋異常!
- @RestController
- public class HelloController {
- @RequestMapping("/upload")
- public void upload(MultipartFile file) {
- if (file == null) {
- throw new CommonException("請選擇上傳文件!");
- }
- //.....
- }
- }
然后寫一個統一異常攔截器,對拋異常的程序進行處理。
這種做法比較直觀,如果當前參數既要判斷是否為空,又要判斷長度是否超過最大長度的時候,代碼就顯得有點多了!
于是,程序界的大佬想到了一個更加優雅又能節省代碼的方式,創建一個斷言類工具類,專門用來判斷參數的是否合法,如果不合法,就拋異常!
- /**
- * 斷言工具類
- */
- public abstract class LocalAssert {
- public static void isTrue(boolean expression, String message) throws CommonException {
- if (!expression) {
- throw new CommonException(message);
- }
- }
- public static void isStringEmpty(String param, String message) throws CommonException{
- if(StringUtils.isEmpty(param)) {
- throw new CommonException(message);
- }
- }
- public static void isObjectEmpty(Object object, String message) throws CommonException {
- if (object == null) {
- throw new CommonException(message);
- }
- }
- public static void isCollectionEmpty(Collection coll, String message) throws CommonException {
- if (coll == null || (coll.size() == 0)) {
- throw new CommonException(message);
- }
- }
- }
當我們需要對參數進行驗證的時候,直接通過這個類就可以完成基本操作,方式如下:
- @RestController
- public class HelloController {
- @RequestMapping("/save")
- public void save(String name, String email) {
- LocalAssert.isStringEmpty(name, "用戶名不能為空!");
- LocalAssert.isStringEmpty(email, "郵箱不能為空!");
- //.....
- }
- }
相比上個步驟,當要判斷的參數比較多時,代碼明顯簡潔多了!
類似這樣的工具類,spring也提供了一個名為Assert的斷言工具類,在開發的時候,可以直接使用!
三、注解驗證
使用注解對數據進行合法性驗證,可以說是 java 界一項非常偉大的創新,使用這種方式不僅使的代碼變得很簡潔,而且閱讀起來非常令人賞心悅目!
3.1、依賴包引入
下面我們一起來看看具體的實踐方式,以Spring Boot工程為例,如果需要使用注解校驗,直接引入spring-boot-starter-web依賴包即可,會自動將注解驗證相關的依賴包打入工程!
- <!-- spring boot web -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
下面在創建實體類的時候,還會用到lombok插件,因此還需要引入lombok依賴包!
- <!-- lombok -->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <version>1.18.4</version>
- <scope>provided</scope>
- </dependency>
如果是普通的Java工程,引入以下幾個依賴包即可!
- <dependency>
- <groupId>org.hibernate.validator</groupId>
- <artifactId>hibernate-validator</artifactId>
- <version>6.0.9.Final</version>
- </dependency>
- <dependency>
- <groupId>javax.el</groupId>
- <artifactId>javax.el-api</artifactId>
- <version>3.0.0</version>
- </dependency>
- <dependency>
- <groupId>org.glassfish.web</groupId>
- <artifactId>javax.el</artifactId>
- <version>2.2.6</version>
- </dependency>
3.2、注解校驗請求對象
緊接著我們來創建一個實體User,用于模擬用戶注冊時的請求實體對象!
- @Data
- @EqualsAndHashCode(callSuper = false)
- @Accessors(chain = true)
- public class User {
- @NotBlank(message = "用戶名不能為空!")
- private String userName;
- @Email(message = "郵箱格式不正確")
- @NotBlank(message = "郵箱不能為空!")
- private String email;
- @NotBlank(message = "密碼不能為空!")
- @Size(min = 8, max = 16,message = "請輸入長度在8~16位的密碼")
- private String userPwd;
- @NotBlank(message = "確認密碼不能為空!")
- private String confirmPwd;
- }
在web層創建一個register()注冊接口方法,同時在請求參數上添加@Valid,如下:
- @RestController
- public class UserController {
- @RequestMapping("/register")
- public boolean register(@RequestBody @Valid User user){
- if(!user.getUserPwd().equals(user.getConfirmPwd())){
- throw new CommonException("確認密碼與密碼不相同,請確認!");
- }
- //業務處理...
- return true;
- }
- }
最后自定義一個異常全局處理器,用于處理異常消息,如下:
- @Slf4j
- @Configuration
- public class GlobalWebMvcConfig implements WebMvcConfigurer {
- /**
- * 統一異常處理
- * @param resolvers
- */
- @Override
- public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
- resolvers.add(new HandlerExceptionResolver() {
- @Override
- public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) {
- log.error("【統一異常攔截】請求出現異常,內容如下:",e);
- ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
- String uri = request.getRequestURI();
- if(e instanceof CommonException){
- //CommonExecption為自定義異常類拋出的異常
- printWrite(((CommonException) e).getMsg(),((CommonException) e).getData(), uri, mv);
- } else if(e instanceof MethodArgumentNotValidException){
- //MethodArgumentNotValidException為注解校驗異常類
- //獲取注解校驗異常信息
- String error = ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage();
- printWrite(error,null, uri, mv);
- } else {
- printWrite(e.getMessage(),null, uri, mv);
- }
- return mv;
- }
- });
- }
- /**
- * 異常封裝相應結果
- * @param object
- */
- private void printWrite(String msg, Object object, String uri, ModelAndView mv){
- ResResult resResult = new ResResult(uri, object);
- if(msg != null){resResult.setMsg(msg);}
- if(log.isDebugEnabled()){
- log.debug("【response】異常輸出結果:" + JSONObject.toJSONString(resResult, SerializerFeature.WriteMapNullValue));
- }
- Map resultMap = BeanToMapUtil.beanToMap(resResult);
- mv.addAllObjects(resultMap);
- }
- }
下面我們啟動項目,使用postman來測試一把,看看效果如何?
- 測試字段是否為空
- 測試郵箱是否合法
- 測試密碼長度是否符合要求
- 測試密碼與確認密碼是否相同
3.3、注解校驗請求參數
上面我們介紹了請求對象的驗證方式,那如果直接在方法上對請求參數進行驗證是否同樣有效呢?
為了眼見為實,下面我們就來模擬在方法上對請求參數進行驗證,看看結果如何。
新建一個查詢接口query,如下
- @RestController
- public class UserController {
- @PostMapping("/query")
- public boolean query(@RequestParam("userId") @Valid @NotBlank(message = "用戶ID不能為空") String userId ){
- return true;
- }
- }
使用postman請求試一試,默認給userId參數為null,結果如下:
很清晰的看到,query()方法中的參數注解驗證無效!
當我們在UserController類上加上@Validated注解!
- @RestController
- @Validated
- public class UserController {
- @PostMapping("/query")
- public boolean query(@RequestParam("userId") @Valid @NotBlank(message = "用戶ID不能為空") String userId ){
- return true;
- }
- }
使用postman請求再試一試,結果如下!
很清晰的看到,注解進行了驗證,同時還拋出異常ConstraintViolationException!
@Validated參數作用于類上時,表示告訴Spring可以對方法中請求參數進行校驗!
所有在實際開發的時候,我們可以使用@Validated和@Valid注解的組合來對方法中的請求參數和請求對象進行校驗!
同時,@Validated和@Valid注解不僅僅只是驗證控制器級別,可以驗證任何Spring組件,例如Service層方法入參的驗證!
- @Service
- @Validated
- public class UserService {
- public void saveUser(@Valid User user){
- //dao插入
- }
- }
3.4、自定義注解驗證
默認的情況下,依賴包已經給我們提供了非常多的校驗注解,如下!
- JSR提供的校驗注解!
- Hibernate Validator提供的校驗注解
但是某些情況,例如性別這個參數可能需要我們自己去驗證,同時我們也可以自定義一個注解來完成參數的校驗,實現方式如下!
- 新創建一個Sex注解,其中SexValidator類指的是具體的參數驗證類
- @Target({FIELD})
- @Retention(RUNTIME)
- @Constraint(validatedBy = SexValidator.class)
- @Documented
- public @interface Sex {
- String message() default "性別值不在可選范圍內";
- Class<?>[] groups() default {};
- Class<? extends Payload>[] payload() default {};
- }
- SexValidator類,實現自ConstraintValidator接口
- public class SexValidator implements ConstraintValidator<Sex, String> {
- @Override
- public boolean isValid(String value, ConstraintValidatorContext context) {
- Set<String> sexSet = new HashSet<String>();
- sexSet.add("男");
- sexSet.add("女");
- return sexSet.contains(value);
- }
- }
最后在User實體類上加入一個性別參數,使用自定義注解進行校驗!
- @Data
- @EqualsAndHashCode(callSuper = false)
- @Accessors(chain = true)
- public class User {
- @NotBlank(message = "用戶名不能為空!")
- private String userName;
- @Email(message = "郵箱格式不正確")
- @NotBlank(message = "郵箱不能為空!")
- private String email;
- @NotBlank(message = "密碼不能為空!")
- @Size(min = 8, max = 16,message = "請輸入長度在8~16位的密碼")
- private String userPwd;
- @NotBlank(message = "確認密碼不能為空!")
- private String confirmPwd;
- /**
- * 自定義注解校驗
- */
- @Sex(message = "性別輸入有誤!")
- private String sex;
- }
使用postman來請求試一試,結果如下!
- 不傳sex參數
很清晰的看到,已經生效!
3.5、手動進行注解校驗
某些時候呢,假如有100個類需要用到校驗注解,此時我們可能在每個類會加上注解@Validated或者@Valid,再增加100個這樣的類,就會造成很多大量的重復工作。
而此時,我們的訴求是想對有校驗注解的實體類進行全局參數驗證!
解決辦法就會用到Validator提供的手動注解校驗證工具類,實現方法如下!
- 新建一個注解驗證工具類
- /**
- * 注解校驗工具類
- */
- public class ValidatorUtils {
- /**
- * 獲取對象中所有注解校驗證異常信息
- * @param object
- * @return
- */
- public static String validated(Object object){
- List<String> errorMessageList = new ArrayList<>();
- //獲取注解校驗工廠
- ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
- Validator validator = factory.getValidator();
- Set<ConstraintViolation<Object>> violations = validator.validate(object);
- for (ConstraintViolation<Object> constraintViolation : violations) {
- errorMessageList.add(constraintViolation.getMessage());
- }
- return errorMessageList.toString();
- }
- }
使用ValidatorUtils工具類,對參數進行驗證
- @Test
- public void testUser(){
- User user = new User();
- System.out.println(ValidatorUtils.validated(user));
- }
執行之后,結果如下!
- [郵箱不能為空!, 用戶名不能為空!, 密碼不能為空!, 確認密碼不能為空!, 性別輸入有誤!]
當然你還可以對ValidatorUtils類進行改造,當有異常信息的時候,直接拋異常!
同時,你還可以通過@Autowired直接注入的方式來獲取Validator對象!
- @Autowired
- Validator validator
3.6、spring 注解校驗原理
如果你對springmvc的方法參數解析器(HandlerMethodArgumentResolver)了解的話,就可能會想到參數校驗這塊肯定是在對應的方法參數解析器里執行的。
直接定位到resolveArgument這個方法,先通過WebDataBinder進行入參屬性綁定,然后再進行校驗!
validateIfApplicable方法邏輯,會遍歷當前參數methodParam所有的注解,如果注解是@Validated或者注解的名字以Valid開頭,則使用WebDataBinder對象執行校驗邏輯。
方法參數解析器只針對接口請求時入參進行驗證,如果想對任何組件中方法進行注解校驗,似乎還缺了點什么!
而當需要對一個類中的方法參數使用注解校驗時,在類上加上@Validated就是為了告訴Spring去校驗方法參數!
底層核心是通過切面代理類并配合MethodValidationPostProcessor這個后置處理器進行處理!
四、總結
參數驗證,在開發中使用非常頻繁,如何優雅的進行驗證,讓代碼變得更加可讀,是業界大佬一直在追求的目標!
本文主要是對自己在項目中的實際使用到參數驗證方式加一整理,希望能幫助到各位網友!
五、參考1、SpringMVC源碼
2、JavaGuide - 如何在 Spring/Spring Boot 中做參數校驗?[1]
3、胡峻崢 - SpringMvc @Validated注解執行原理[2]
參考資料
[1]JavaGuide - 如何在 Spring/Spring Boot 中做參數校驗?: https://juejin.im/post/5dc8bc745188254e7a155ba0#heading-14[2]胡峻崢 - SpringMvc @Validated注解執行原理: https://www.cnblogs.com/hujunzheng/p/12570921.html