上海公司注冊(cè)代辦一般多少錢(qián)優(yōu)化推廣
作者簡(jiǎn)介:大家好,我是smart哥,前中興通訊、美團(tuán)架構(gòu)師,現(xiàn)某互聯(lián)網(wǎng)公司CTO
聯(lián)系qq:184480602,加我進(jìn)群,大家一起學(xué)習(xí),一起進(jìn)步,一起對(duì)抗互聯(lián)網(wǎng)寒冬
寫(xiě)一個(gè)接口,大致就幾個(gè)步驟:
- 參數(shù)校驗(yàn)
- 編寫(xiě)Service、Dao(SQL)
- Result封裝返回值
- 如果是分布式,還可能涉及網(wǎng)關(guān)配置、服務(wù)引用等
業(yè)務(wù)代碼總是變化的,沒(méi)太多可說(shuō)的,統(tǒng)一結(jié)果封裝我們已經(jīng)介紹過(guò),今天我們來(lái)聊聊參數(shù)校驗(yàn)的瑣事。
老實(shí)說(shuō),參數(shù)校驗(yàn)很煩!不校驗(yàn)不行,仔細(xì)校驗(yàn)吧,代碼又顯得非常冗余,很丑:
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {if (user == null) {return Result.error(ExceptionCodeEnum.EMPTY_PARAM);}if (user.getId() == null || user.getId() <= 0) {return Result.error("id為空或小于0");}if (StringUtils.isEmpty(user.getName()) || user.getName().length() > 4) {return Result.error("姓名不符合規(guī)范");}if (user.getAge() < 18) {return Result.error("年齡不小于18");}if (StringUtils.isEmpty(user.getPhone()) || user.getPhone().length() != 11) {return Result.error("手機(jī)號(hào)碼不正確");}return Result.success(userService.save(user));
}
但無(wú)論以什么方式進(jìn)行參數(shù)校驗(yàn),歸根到底就是兩種:
- 手動(dòng)校驗(yàn)
- 自動(dòng)校驗(yàn)
對(duì)應(yīng)到實(shí)際編碼的話,推薦:
- 封裝ValidatorUtils
- 使用Spring Validation
其實(shí)對(duì)于上面兩種方式,Spring都提供了解決方案。很多人只知道Spring Validation,卻不知道簡(jiǎn)單好用的Assert。
public class SpringAssertTest {/*** Spring提供的Assert工具類,可以指定IllegalArgumentException的message** @param args*/public static void main(String[] args) {String name = "";
// Assert.hasText(name, "名字不能為空");Integer age = null;
// Assert.notNull(age, "年齡不能為空");Integer height = 180;Assert.isTrue(height > 185, "身高不能低于185");}
}
只要在全局異常處理IllegalArgumentException即可。但個(gè)人覺(jué)得還是自己封裝自由度高一些,所以我們按照這個(gè)思路,寫(xiě)一個(gè)ValidatorUtils。
封裝ValidatorUtils
封裝ValidatorUtils也有兩種思路:
- 校驗(yàn)并返回結(jié)果,調(diào)用者自行處理
- 校驗(yàn)失敗直接拋異常
方式一:校驗(yàn)并返回結(jié)果,調(diào)用者自行處理
比如,方法只返回true/false:
public final class ValidatorUtils {private ValidatorUtils() {}/*** 校驗(yàn)id是否合法** @param id*/public static boolean isNotId(Long id) {if (id == null) {return true;}if (id < 0) {return true;}return false;}
}
調(diào)用者根據(jù)返回值自行處理(拋異?;蛘哂肦esult封裝):
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {if (user == null) {return Result.error(ExceptionCodeEnum.EMPTY_PARAM);}// 對(duì)校驗(yàn)結(jié)果進(jìn)行判斷并返回,也可以拋異常讓@RestControllerAdvice兜底if (ValidatorUtils.isNotId(user.getId())) {return Result.error("id為空或小于0");}return Result.success(userService.save(user));
}
這種方式,本質(zhì)上和不封裝差不多...
方式二:校驗(yàn)失敗直接拋異常
這種形式一般會(huì)結(jié)合@RestControllerAdvice進(jìn)行全局異常處理:
public final class ValidatorUtils {private ValidatorUtils() {}// 錯(cuò)誤信息模板private static final String IS_EMPTY = "%s不能為空";private static final String LESS_THAN_ZERO = "%s不能小于0";/*** 校驗(yàn)參數(shù)是否為null** @param param* @param fieldName*/public static void checkNull(Object param, String fieldName) {if (param == null) {// ValidatorException是自定義異常throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}/*** 校驗(yàn)id是否合法** @param id* @param fieldName*/public static void checkId(Long id, String fieldName) {if (id == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (id < 0) {throw new ValidatorException(String.format(LESS_THAN_ZERO, fieldName));}}
}
@PostMapping("updateUser")
public Result<Boolean> updateUser(@RequestBody User user) {// 一連串的校驗(yàn)ValidatorUtils.checkNull(user, "user");ValidatorUtils.checkId(user.getId(), "用戶id");return Result.success(true);
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略業(yè)務(wù)異常、運(yùn)行時(shí)異常處理*//*** ValidatorUtils校驗(yàn)異常* @see ValidatorUtils** @param e* @return*/@ExceptionHandler(ValidatorException.class)public Result<ExceptionCodeEnum> handleValidatorException(ValidatorException e) {// 打印精確的參數(shù)錯(cuò)誤日志,方便后端排查log.warn("參數(shù)校驗(yàn)異常: {}", e.getMessage(), e);// 一般來(lái)說(shuō),給客戶端展示“參數(shù)錯(cuò)誤”等泛化的錯(cuò)誤信息即可,聯(lián)調(diào)時(shí)可以返回精確的信息:e.getMessage()return Result.error(ExceptionCodeEnum.ERROR_PARAM);}
}
代碼
具體選擇哪種,看個(gè)人喜好啦。這里給出第二種封裝形式(也可以改成第一種):
public final class ValidatorUtils {private ValidatorUtils() {}private static final String IS_EMPTY = "%s不能為空";private static final String LESS_THAN_ZERO = "%s不能小于0";private static final String LENGTH_OUT_OF_RANGE = "%s長(zhǎng)度要在%d~%d之間";private static final String LENGTH_LESS_THAN = "%s長(zhǎng)度不能小于%d";private static final String LENGTH_GREATER_THAN = "%s長(zhǎng)度不能大于%d";private static final String ILLEGAL_PARAM = "%s不符合規(guī)則";// 手機(jī)號(hào)碼正則,可以根據(jù)需要自行調(diào)整public static final String MOBILE = "1\\d{10}";/*** 是否為true** @param expression* @param message*/public static void isTrue(boolean expression, String message) {if (!expression) {throw new ValidatorException(message);}}/*** 校驗(yàn)參數(shù)是否為null** @param param* @param fieldName*/public static void checkNull(Object param, String fieldName) {if (param == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}/*** 校驗(yàn)參數(shù)是否為null或empty** @param param* @param fieldName*/public static void checkNullOrEmpty(Object param, String fieldName) {if (param == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (param instanceof CharSequence) {if (param instanceof String && "null".equals(((String) param).toLowerCase())) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (isBlank((CharSequence) param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}if (isCollectionsSupportType(param) && sizeIsEmpty(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}}/*** 校驗(yàn)id是否合法** @param id* @param fieldName*/public static void checkId(Long id, String fieldName) {if (id == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (id < 0) {throw new ValidatorException(String.format(LESS_THAN_ZERO, fieldName));}}/*** 校驗(yàn)id是否合法** @param id* @param fieldName*/public static void checkId(Integer id, String fieldName) {if (id == null) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (id < 0) {throw new ValidatorException(String.format(LESS_THAN_ZERO, fieldName));}}/*** 校驗(yàn)參數(shù)字符串** @param param 字符串參數(shù)* @param min 最小長(zhǎng)度* @param max 最大長(zhǎng)度*/public static void checkLength(String param, int min, int max, String fieldName) {if (param == null || "".equals(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}int length = param.length();if (length < min || length > max) {throw new ValidatorException(String.format(LENGTH_OUT_OF_RANGE, fieldName, min, max));}}/*** 校驗(yàn)參數(shù)字符串** @param param 字符串參數(shù)* @param min 最小長(zhǎng)度*/public static void checkMinLength(String param, int min, String fieldName) {if (param == null || "".equals(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (param.length() < min) {throw new ValidatorException(String.format(LENGTH_LESS_THAN, fieldName, min));}}/*** 校驗(yàn)參數(shù)字符串** @param param 字符串參數(shù)* @param max 最大長(zhǎng)度*/public static void checkMaxLength(String param, int max, String fieldName) {if (param == null || "".equals(param)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}if (param.length() > max) {throw new ValidatorException(String.format(LENGTH_GREATER_THAN, fieldName, max));}}/*** 校驗(yàn)手機(jī)號(hào)是否合法** @param phone 手機(jī)號(hào)*/public static void checkPhone(String phone, String fieldName) {if (phone == null || "".equals(phone)) {throw new ValidatorException(String.format(IS_EMPTY, fieldName));}boolean matches = Pattern.matches(MOBILE, phone);if (!matches) {throw new ValidatorException(String.format(ILLEGAL_PARAM, fieldName));}}// --------- private method ----------private static boolean isBlank(CharSequence cs) {int strLen;if (cs != null && (strLen = cs.length()) != 0) {for (int i = 0; i < strLen; ++i) {if (!Character.isWhitespace(cs.charAt(i))) {return false;}}}return true;}private static boolean sizeIsEmpty(final Object object) {if (object == null) {return true;} else if (object instanceof Collection<?>) {return ((Collection<?>) object).isEmpty();} else if (object instanceof Map<?, ?>) {return ((Map<?, ?>) object).isEmpty();} else if (object instanceof Object[]) {return ((Object[]) object).length == 0;} else {try {return Array.getLength(object) == 0;} catch (final IllegalArgumentException ex) {throw new IllegalArgumentException("Unsupported object type: " + object.getClass().getName());}}}private static boolean isCollectionsSupportType(Object value) {boolean isCollectionOrMap = value instanceof Collection || value instanceof Map;return isCollectionOrMap || value.getClass().isArray();}
}
@Getter
@NoArgsConstructor
public class ValidatorException extends RuntimeException {/*** 自定義業(yè)務(wù)錯(cuò)誤碼*/private Integer code;/*** 系統(tǒng)源異常*/private Exception originException;/*** 完整的構(gòu)造函數(shù):參數(shù)錯(cuò)誤碼+參數(shù)錯(cuò)誤信息+源異常信息** @param code 參數(shù)錯(cuò)誤碼* @param message 參數(shù)錯(cuò)誤信息* @param originException 系統(tǒng)源異常*/public ValidatorException(Integer code, String message, Exception originException) {super(message);this.code = code;this.originException = originException;}/*** 構(gòu)造函數(shù):錯(cuò)誤枚舉+源異常信息** @param codeEnum*/public ValidatorException(ExceptionCodeEnum codeEnum, Exception originException) {this(codeEnum.getCode(), codeEnum.getDesc(), originException);}/*** 構(gòu)造函數(shù):參數(shù)錯(cuò)誤信息+源異常信息** @param message 參數(shù)錯(cuò)誤信息* @param originException 系統(tǒng)源錯(cuò)誤*/public ValidatorException(String message, Exception originException) {this(ExceptionCodeEnum.ERROR_PARAM.getCode(), message, originException);}/*** 構(gòu)造函數(shù):錯(cuò)誤枚舉** @param codeEnum 錯(cuò)誤枚舉*/public ValidatorException(ExceptionCodeEnum codeEnum) {this(codeEnum.getCode(), codeEnum.getDesc(), null);}/*** 構(gòu)造函數(shù):參數(shù)錯(cuò)誤信息** @param message 參數(shù)錯(cuò)誤信息*/public ValidatorException(String message) {this(ExceptionCodeEnum.ERROR_PARAM.getCode(), message, null);}
}
Spring Validation
Spring也封裝了一套基于注解的參數(shù)校驗(yàn)邏輯,常用的有:
- @Validated
- @NotNull
- @NotBlank
- @NotEmpty
- @Positive
- @Length
- @Max
- @Min
大家可能之前聽(tīng)說(shuō)過(guò)@Valid,它和@Validated有什么關(guān)系呢?@Valid是JSR303規(guī)定的,@Validated是Spring擴(kuò)展的,@Validated相對(duì)來(lái)說(shuō)功能更加強(qiáng)大,推薦優(yōu)先使用@Validated。
SpringBoot2.3.x之前可以直接使用@Validated及@Valid,SpringBoot2.3.x以后需要額外引入依賴:
<dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>6.0.1.Final</version>
</dependency>
GET散裝參數(shù)校驗(yàn):ConstraintViolationException
實(shí)際開(kāi)發(fā)中,如果某個(gè)GET接口只有一兩個(gè)參數(shù),可以使用“散裝”的參數(shù)列表(注意類上加@Validated):
@Slf4j
@Validated
@RestController
public class UserController {@GetMapping("getUser")public Result<User> getUser(@NotNull(message = "部門(mén)id不能為空") Long departmentId,@NotNull(message = "年齡不能為空")@Max(value = 35, message = "年齡不超過(guò)35")@Min(value = 18, message = "年齡不小于18") Integer age) {return Result.success(null);}
}
如果@RestControllerAdvice沒(méi)有捕獲對(duì)應(yīng)的異常,會(huì)返回SpringBoot默認(rèn)的異常JSON:
服務(wù)端則拋出ConstraintViolationException:
這樣的提示不夠友好,我們可以按之前的思路,為ConstraintViolationException進(jìn)行全局異常處理:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略業(yè)務(wù)異常、運(yùn)行時(shí)異常等其他異常處理*//*** ConstraintViolationException異常** @param e* @return* @see ValidatorUtils*/@ExceptionHandler(ConstraintViolationException.class)public Result<ExceptionCodeEnum> handleConstraintViolationException(ConstraintViolationException e) {log.warn("參數(shù)錯(cuò)誤: {}", e.getMessage(), e);// 一般只需返回泛化的錯(cuò)誤信息,比如“參數(shù)錯(cuò)誤”return Result.error(ExceptionCodeEnum.ERROR_PARAM, e.getMessage());}
}
格式覺(jué)得丑的話,可以自己調(diào)整。
GET DTO參數(shù)校驗(yàn):BindException
@Data
public class User {@NotNull(message = "id不能為空")private Long id;@NotNull(message = "年齡不能為空")@Max(value = 35, message = "年齡不超過(guò)35")@Min(value = 18, message = "年齡不小于18")private Integer age;
}
@Slf4j
@RestController
public class UserController {/*** 如果都是用DTO包裝參數(shù),那么Controller可以不加@Validated(但建議還是都加上吧)* 參數(shù)列表里用@Validated或@Valid都可以** @param user* @return*/@GetMapping("getUser")public Result<User> getUser(@Validated User user) {System.out.println("進(jìn)來(lái)了");return Result.success(null);}
}
你會(huì)發(fā)現(xiàn),雖然參數(shù)校驗(yàn)確實(shí)生效了:
但是全局異常似乎沒(méi)有捕獲到這個(gè)異常,最終又交給了SpringBoot處理:
{"timestamp": "2021-02-08T02:57:27.025+00:00","status": 400,"error": "Bad Request","message": "","path": "/getUser"
}
這是怎么回事呢?
實(shí)際上,從GET“散裝參數(shù)”變成“DTO參數(shù)”后,校驗(yàn)異常從ConstraintViolationException變成了BindException(見(jiàn)上面的截圖),所以需要另外定義:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略業(yè)務(wù)異常、運(yùn)行時(shí)異常等其他異常處理*//*** BindException異常** @param e* @return*/@ExceptionHandler(BindException.class)public Result<Map<String, String>> validationBindException(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("參數(shù)錯(cuò)誤: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}
}
重新請(qǐng)求:
{"code": 10000,"message": "id不能為空 && 年齡不小于18","data": null
}
POST參數(shù)校驗(yàn):MethodArgumentNotValidException
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated @RequestBody User user) {System.out.println("進(jìn)來(lái)了");return Result.success(null);
}
和GET DTO參數(shù)校驗(yàn)形式上一樣,但POST校驗(yàn)的異常又是另一種,所以全局異常處理又要加一種:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/* 省略業(yè)務(wù)異常、運(yùn)行時(shí)異常等其他異常處理*//*** MethodArgumentNotValidException異常** @param e* @return*/@ExceptionHandler(MethodArgumentNotValidException.class)public Result<Map<String, String>> validationMethodArgumentNotValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("參數(shù)錯(cuò)誤: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}
}
代碼
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 業(yè)務(wù)異常** @param* @return*/@ExceptionHandler(BizException.class)public Result<ExceptionCodeEnum> handleBizException(BizException bizException) {log.warn("業(yè)務(wù)異常:{}", bizException.getMessage(), bizException);return Result.error(bizException.getError());}/*** 運(yùn)行時(shí)異常** @param e* @return*/@ExceptionHandler(RuntimeException.class)public Result<ExceptionCodeEnum> handleRunTimeException(RuntimeException e) {log.warn("運(yùn)行時(shí)異常: {}", e.getMessage(), e);return Result.error(ExceptionCodeEnum.ERROR);}/*** ValidatorUtils校驗(yàn)異常** @param e* @return* @see ValidatorUtils*/@ExceptionHandler(ValidatorException.class)public Result<ExceptionCodeEnum> handleValidatorException(ValidatorException e) {// 打印精確的參數(shù)錯(cuò)誤日志,方便后端排查log.warn("參數(shù)校驗(yàn)異常: {}", e.getMessage(), e);// 一般來(lái)說(shuō),給客戶端展示泛化的錯(cuò)誤信息即可,聯(lián)調(diào)時(shí)可以返回精確的信息return Result.error(e.getMessage());}/*** ConstraintViolationException異常(散裝GET參數(shù)校驗(yàn))** @param e* @return* @see ValidatorUtils*/@ExceptionHandler(ConstraintViolationException.class)public Result<ExceptionCodeEnum> handleConstraintViolationException(ConstraintViolationException e) {log.warn("參數(shù)錯(cuò)誤: {}", e.getMessage(), e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, e.getMessage());}/*** BindException異常(GET DTO校驗(yàn))** @param e* @return*/@ExceptionHandler(BindException.class)public Result<Map<String, String>> validationBindException(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("參數(shù)錯(cuò)誤: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}/*** MethodArgumentNotValidException異常(POST DTO校驗(yàn))** @param e* @return*/@ExceptionHandler(MethodArgumentNotValidException.class)public Result<Map<String, String>> validationMethodArgumentNotValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String message = fieldErrors.stream().map(FieldError::getDefaultMessage).collect(Collectors.joining(" && "));log.error("參數(shù)錯(cuò)誤: {}", message, e);return Result.error(ExceptionCodeEnum.ERROR_PARAM, message);}}
其他校驗(yàn)場(chǎng)景
Spring Validation還有一些校驗(yàn)場(chǎng)景,這里補(bǔ)充一下:
- 嵌套校驗(yàn)
- 分組校驗(yàn)
- List校驗(yàn)
嵌套校驗(yàn)
@Validated不支持嵌套校驗(yàn),只能用@Valid:
@Data
public class User {@NotNull(message = "id不能為空")private Long id;@NotNull(message = "年齡不能為空")@Max(value = 35, message = "年齡不超過(guò)35")@Min(value = 18, message = "年齡不小于18")private Integer age;@NotNull(message = "所屬部門(mén)不能為空")@Validprivate Department department;@Datastatic class Department {@NotNull(message = "部門(mén)編碼不能為空")private Integer sn;@NotBlank(message = "部門(mén)名稱不能為空")private String name;}
}
分組校驗(yàn)
@Data
public class User {@NotNull(message = "id不能為空", groups = {Update.class})private Long id;@NotNull(message = "年齡不能為空", groups = {Add.class, Update.class})@Max(value = 35, message = "年齡不超過(guò)35", groups = {Add.class, Update.class})@Min(value = 18, message = "年齡不小于18", groups = {Add.class, Update.class})private Integer age;public interface Add {}public interface Update {}
}
@Slf4j
@RestController
public class UserController {@PostMapping("insertUser")public Result<Boolean> insertUser(@Validated(User.Add.class) @RequestBody User user) {System.out.println("進(jìn)來(lái)了");return Result.success(null);}@PostMapping("updateUser")public Result<Boolean> updateUser(@Validated(User.Update.class) @RequestBody User user) {System.out.println("進(jìn)來(lái)了");return Result.success(null);}
}
有兩點(diǎn)需要注意:
- interface Add這些接口只是做個(gè)標(biāo)記,本身沒(méi)有任何實(shí)際意義,可以抽取出來(lái),作為單獨(dú)的接口復(fù)用
- interface Add還可以繼承Default接口
@Data
public class User {// 只在Update分組下生效@NotNull(message = "id不能為空", groups = {Update.class})private Long id;// 此時(shí)如果沒(méi)執(zhí)行Group,那么無(wú)論什么分組,都會(huì)校驗(yàn)@NotNull(message = "年齡不能為空")@Max(value = 35, message = "年齡不超過(guò)35")@Min(value = 18, message = "年齡不小于18")private Integer age;public interface Add extends Default {}public interface Update extends Default {}
}
繼承Default后,除非顯示指定,否則只要加了@NotNull等注解,就會(huì)起效。但顯示指定Group后,就按指定的分組進(jìn)行校驗(yàn)。比如,上面的id只會(huì)在update時(shí)校驗(yàn)生效。
個(gè)人不建議繼承Default,一方面是理解起來(lái)比較亂,另一方是加了Default后就無(wú)法進(jìn)行部分字段更新了。比如:
@PostMapping("updateUser")
public Result<Boolean> updateUser(@Validated(User.Update.class) @RequestBody User user) {System.out.println("進(jìn)來(lái)了");return Result.success(null);
}
@Data
public class User {@NotNull(message = "id不能為空", groups = {Update.class})private Long id;@NotNull(message = "年齡不能為空")private Integer age;@NotBlank(message = "住址不能為空")private String address;public interface Add extends Default {}public interface Update extends Default {}
}
此時(shí)如果想更新name,就不能只傳id和name了,address也要傳(默認(rèn)也會(huì)校驗(yàn))。當(dāng)然,你也可以認(rèn)為一般情況下update前都會(huì)有g(shù)etById(),所以更新時(shí)數(shù)據(jù)也是全量的。
List校驗(yàn)
Spring Validation不支持以下方式校驗(yàn):
@Data
public class User {@NotNull(message = "id不能為空")private Long id;@NotNull(message = "年齡不能為空")private Integer age;
}
@PostMapping("updateBatchUser")
public Result<Boolean> updateBatchUser(@Validated @RequestBody List<User> list) {System.out.println(list);return Result.success(null);
}
即使age不填,還是進(jìn)來(lái)了,說(shuō)明對(duì)于List而言,@Validated根本沒(méi)作用:
解決辦法是,借鑒嵌套校驗(yàn)的模式,在List外面再包一層:
@PostMapping("updateBatchUser")
public Result<Boolean> updateBatchUser(@Validated @RequestBody ValidationList<User> userList) {System.out.println(userList);return Result.success(null);
}
public class ValidationList<E> implements List<E> {@NotEmpty(message = "參數(shù)不能為空")@Validprivate List<E> list = new LinkedList<>();@Overridepublic int size() {return list.size();}@Overridepublic boolean isEmpty() {return list.isEmpty();}@Overridepublic boolean contains(Object o) {return list.contains(o);}@Overridepublic Iterator<E> iterator() {return list.iterator();}@Overridepublic Object[] toArray() {return list.toArray();}@Overridepublic <T> T[] toArray(T[] a) {return list.toArray(a);}@Overridepublic boolean add(E e) {return list.add(e);}@Overridepublic boolean remove(Object o) {return list.remove(o);}@Overridepublic boolean containsAll(Collection<?> c) {return list.containsAll(c);}@Overridepublic boolean addAll(Collection<? extends E> c) {return list.addAll(c);}@Overridepublic boolean addAll(int index, Collection<? extends E> c) {return list.addAll(index, c);}@Overridepublic boolean removeAll(Collection<?> c) {return list.removeAll(c);}@Overridepublic boolean retainAll(Collection<?> c) {return list.retainAll(c);}@Overridepublic void clear() {list.clear();}@Overridepublic E get(int index) {return list.get(index);}@Overridepublic E set(int index, E element) {return list.set(index, element);}@Overridepublic void add(int index, E element) {list.add(index, element);}@Overridepublic E remove(int index) {return list.remove(index);}@Overridepublic int indexOf(Object o) {return list.indexOf(o);}@Overridepublic int lastIndexOf(Object o) {return list.lastIndexOf(o);}@Overridepublic ListIterator<E> listIterator() {return list.listIterator();}@Overridepublic ListIterator<E> listIterator(int index) {return list.listIterator(index);}@Overridepublic List<E> subList(int fromIndex, int toIndex) {return list.subList(fromIndex, toIndex);}public List<E> getList() {return list;}public void setList(List<E> list) {this.list = list;}}
實(shí)際開(kāi)發(fā)時(shí),建議專門(mén)建一個(gè)package存放Spring Validation相關(guān)的接口和類:
SpringValidatorUtils封裝
一起來(lái)封裝一個(gè)SpringValidatorUtils:
public final class SpringValidatorUtils {private SpringValidatorUtils() {}/*** 校驗(yàn)器*/private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();/*** 校驗(yàn)參數(shù)** @param param 待校驗(yàn)的參數(shù)* @param groups 分組校驗(yàn),比如Update.class(可以不傳)* @param <T>*/public static <T> void validate(T param, Class<?>... groups) {Set<ConstraintViolation<T>> validateResult = validator.validate(param, groups);if (!CollectionUtils.isEmpty(validateResult)) {StringBuilder validateMessage = new StringBuilder();for (ConstraintViolation<T> constraintViolation : validateResult) {validateMessage.append(constraintViolation.getMessage()).append(" && ");}// 去除末尾的 &&validateMessage.delete(validateMessage.length() - 4, validateMessage.length());// 拋給全局異常處理throw new ValidatorException(validateMessage.toString());}}
}
代碼很簡(jiǎn)單,做的事情本質(zhì)是和@Validated是一模一樣的。@Validated通過(guò)注解方式讓Spring使用Validator幫我們校驗(yàn),而SpringValidatorUtils則是我們從Spring那借來(lái)Validator自己校驗(yàn):
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {SpringValidatorUtils.validate(user);System.out.println("進(jìn)來(lái)了");return Result.success(null);
}
此時(shí)不需要加@Validated。
買(mǎi)一送一,看看我之前一個(gè)同事封裝的工具類(更加自由,調(diào)用者決定拋異常還是返回錯(cuò)誤信息):
public final class ValidationUtils {private static final Validator DEFAULT_VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();private ValidationUtils() {}/*** 驗(yàn)證基于注解的對(duì)象** @param target*/public static <T> String validateReq(T target, boolean throwException) {if (null == target) {return errorProcess("校驗(yàn)對(duì)象不能為空", throwException);} else {Set<ConstraintViolation<T>> constraintViolations = DEFAULT_VALIDATOR.validate(target);ConstraintViolation<T> constraintViolation = Iterables.getFirst(constraintViolations, null);if (constraintViolation != null) {// 用戶可以指定拋異常還是返回錯(cuò)誤信息return errorProcess(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage(),throwException);}}return "";}private static String errorProcess(String errorMsg, boolean throwException) {if (throwException) {throw new InvalidParameterException(errorMsg);}return errorMsg;}
}
OK,至此對(duì)Spring Validation的介紹結(jié)束。
為什么@Validated這么方便,還要封裝這個(gè)工具類呢?首先,很多人搞不清楚@Validated的使用或者覺(jué)得注解很礙眼,不喜歡。其次,也是最重要的,如果你想在Service層做校驗(yàn),使用SpringValidatorUtils會(huì)方便些(Service有接口和實(shí)現(xiàn)類,麻煩些)。當(dāng)然,Service也能用注解方式校驗(yàn)。
參數(shù)校驗(yàn)就介紹到這,有更好的方式歡迎大家評(píng)論交流。我個(gè)人曾經(jīng)特別喜歡Spring Validation,后來(lái)覺(jué)得其實(shí)使用工具類也蠻好,想校驗(yàn)啥就寫(xiě)啥,很細(xì)膩,不用考慮亂七八糟的分組,而Spring Validation有時(shí)需要花費(fèi)很多心思在分組上,就有點(diǎn)本末倒置了。
最后拋出兩個(gè)問(wèn)題:
- 寫(xiě)完才發(fā)現(xiàn),ValidatorUtils竟然用了static final抽取錯(cuò)誤信息模板,然后利用String.format()拼接。會(huì)出現(xiàn)線程安全問(wèn)題嗎?
- 你知道如何設(shè)計(jì)山寨版的Spring Validation嗎?(只需要實(shí)現(xiàn)@NotNull + ValidatorUtils,參考答案見(jiàn)評(píng)論區(qū))
作者簡(jiǎn)介:大家好,我是smart哥,前中興通訊、美團(tuán)架構(gòu)師,現(xiàn)某互聯(lián)網(wǎng)公司CTO
進(jìn)群,大家一起學(xué)習(xí),一起進(jìn)步,一起對(duì)抗互聯(lián)網(wǎng)寒冬