二胖寫參數(shù)校驗(yàn)的坎坷之路
掃描二維碼
隨時(shí)隨地手機(jī)看文章
背景
最近端午好久沒有和二胖聚一聚了,于是約了二胖到人民廣場去宰他一頓,正好最近他跳槽加薪了。
我:二胖聽說你最近跳槽了,并且還是從傳統(tǒng)軟件公司跳到了互聯(lián)網(wǎng)公司,工資是不是漲了一點(diǎn)啊,今天你請客哈。
二胖:別說了,工資是漲了點(diǎn),但是性價(jià)比反而變低了,以前到點(diǎn)就下班,現(xiàn)在下班到家都快12點(diǎn)了。
我:新公司怎么樣還適應(yīng)嗎?除了上班時(shí)間久點(diǎn)。
二胖:哎,這個(gè)還真稍微有點(diǎn)不適應(yīng),這不是剛進(jìn)去沒啥事,leader
就給我安排了一個(gè)簡單的用戶保存功能,原來以前公司個(gè)把小時(shí)就做好了的功能,在這新公司硬是折騰了兩三天,真是苦不堪言。我改了好幾個(gè)版本最終leader
才滿意的點(diǎn)了點(diǎn)頭。
接口裸奔
-
按照二胖在以前公司的寫法再傳統(tǒng)公司反正系統(tǒng)都是服務(wù)內(nèi)部人員的,在后端寫參數(shù)校驗(yàn)是不存在的事情,完全信賴前端傳過來的內(nèi)容。這不寫完代碼自測一把發(fā)現(xiàn)可以保存數(shù)據(jù),就屁顛屁顛的發(fā)起代碼 review
了(二胖在以前的公司代碼review
是不存在的,只要功能實(shí)現(xiàn)就好了)。正好leader
今天有點(diǎn)時(shí)間,看到新同事提交的代碼看看寫的怎么樣??粗@個(gè)裸奔的接口,leader
把二胖叫了過去,語重心長的跟二胖說道:"你這個(gè)參數(shù)校驗(yàn)不寫寫嗎?不怕人家攻擊你的接口嗎?這里不校驗(yàn),直接用,不怕引入sql注入嗎?這里不校驗(yàn)下郵箱是否符合格式嗎?這個(gè)判空也不寫,不怕大量的空指針,服務(wù)熔斷嗎?..."。面對leader
的拼命十三問,二胖心想試用期怕是有點(diǎn)難過哦?只能低著頭回到工位重新按照leader
的教育整改起來,然后又重新提交了。
參數(shù)校驗(yàn)if判斷
leader
看了看說到:“這次代碼比上次好多了,功能基本沒啥問題了,但是這一塊代碼是不是可以在優(yōu)化下,這樣寫不是很優(yōu)雅”
if(Objects.isNull(user)){
throw new IllegalArgumentException("用戶不能為空");
}
if(StringUtils.isEmpty(user.getUserName())){
throw new IllegalArgumentException("用戶名不能為空");
}
if(StringUtils.isEmpty(user.getUserName())){
throw new IllegalArgumentException("用戶名不能為空");
}
if(StringUtils.isEmpty(user.getSex())){
throw new IllegalArgumentException("用戶性別不能為空");
}
if(Objects.isNull(user.getUserDetail())){
throw new IllegalArgumentException("用戶詳細(xì)信息不能為空");
}
if(Objects.isNull(user.getUserDetail().getAddress())){
throw new IllegalArgumentException("用戶地址不能為空");
}
if(!"M".equals(user.getSex()) && !"F".equals(user.getSex())){
throw new IllegalArgumentException("用戶性別不合法");
}
二胖也是一陣郁悶,還是懷念以前的公司啊,功能實(shí)現(xiàn)就好,代碼想怎么寫就怎么寫?;ヂ?lián)網(wǎng)公司就是規(guī)矩多,寫完代碼還要寫單測,還要監(jiān)控一堆破事,活該這群人996.時(shí)間都花到這上面去了。抱怨該抱怨但是代碼還得改啊。現(xiàn)在疫情期間好不容易找一個(gè)工作不能丟啊。二胖想到以前不是學(xué)過aop
嗎?再配合下自定義注解,這樣代碼就應(yīng)該比較優(yōu)雅了吧,說干就干。
自定義注解實(shí)現(xiàn)
-
首先自定義了一個(gè)注解因?yàn)橐r?yàn)參數(shù)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
public @interface ParameterValidator {
}
-
配置一個(gè)切面,解析有 ParameterValidator
注解的方法。然后通過切面獲取所有請求的參數(shù),獲取參數(shù)之后就解析參數(shù)上面的注解。配置切面啥的都比較簡單,稍微復(fù)雜的就是反射解析參數(shù)了,因?yàn)橐婕暗秸埱髤?shù)的嵌套結(jié)構(gòu)。二胖習(xí)慣性的面向百度編程能copy
別人的代碼堅(jiān)決不去自己寫。百度出來的基本上都是單層結(jié)構(gòu),簡單基本類型的對象,沒有涉及到是嵌套、級聯(lián)的類型的參數(shù)。最后在github
( 全球最大的同性交友網(wǎng)站)找了一圈也沒有找到合適的。既然拿來主義沒有結(jié)果那就只能哼次哼次的自己寫了,幸好自己以前學(xué)過點(diǎn)反射的知識?;艘粋€(gè)小時(shí)通過遞歸調(diào)用寫了個(gè)粗糙的版本,比較粗糙還有很多場景沒有考慮進(jìn)去。不過基本可以滿足條件了部分代碼如下:
public static void checkField(Object object, Class<?> aClass) throws IllegalAccessException {
boolean primitive = isPrimitive(aClass);
if (primitive) {
return;
}
Field[] declaredFields = filterField(aClass.getDeclaredFields());
for (Field field : declaredFields) {
makeAccessible(field);
// 校驗(yàn)我們自定義注解
MyNotBlank fieldAnnotation = field.getAnnotation(MyNotBlank.class);
Object currentObject = field.get(object);
if (Objects.nonNull(fieldAnnotation)) {
if (StringUtils.isEmpty(currentObject)) {
throw new IllegalArgumentException(field.getName()+":"+fieldAnnotation.message());
}
}
if (!isJavaClass(field.getType())) {
// 遞歸調(diào)用,有級聯(lián)參數(shù)時(shí)候
checkField(currentObject);
} else if (field.getType().isPrimitive()) {
} else if (field.getType().isAssignableFrom(List.class)) {
// 遞歸調(diào)用,解析list類型
getList(field, currentObject);
}
}
}
然后趕緊測試一波,還不錯(cuò)基本功能實(shí)現(xiàn)了,能夠?qū)崿F(xiàn)判空檢驗(yàn)了,也可以實(shí)現(xiàn)級聯(lián)校驗(yàn)了。效果如下:不過這個(gè)現(xiàn)在支持類型為基本類型和
String
、List
的 后續(xù)如果參數(shù)類型是數(shù)組、或者Map
等等還得去解析。這時(shí)候同事二狗從旁邊走過,看到二胖這么認(rèn)真的在敲代碼。
二狗:二胖你又在寫什么bug
啊。
二胖:在自己造個(gè)輪子,寫個(gè)通用的參數(shù)校驗(yàn)。
二狗:這個(gè)現(xiàn)在市面上不是已經(jīng)有現(xiàn)成的方案了嗎?jsr(Java Specification Requests)
可以去了解下哦。
二胖:好的我馬上去查下資料。
jsr(Java Specification Requests) Java 規(guī)范提案
-
說到 jsr
我們就得先了解下什么是JCP(Java Community Process)
?
JCP(Java Community Process) 是一個(gè)開放的國際組織,主要由Java開發(fā)者以及被授權(quán)者組成,職能是發(fā)展和更新。
-
JSR又是個(gè)什么東東列?
它是指向JCP提出新增一個(gè)標(biāo)準(zhǔn)化技術(shù)規(guī)范的正式請求。任何人都可以提交JSR,(如果你覺得自己牛逼你也可以提交一個(gè)) 以向Java平臺增添新的API和服務(wù)。JSR已成為Java界的一個(gè)重要標(biāo)準(zhǔn)。
Bean Validation
Bean Validation 顧名思義是對 java Bean 的校驗(yàn),目前為止,Java 對 Bean 的校驗(yàn)有3個(gè)規(guī)范。
-
JSR-303 : Bean Validation -
JSR 349 : Bean Validation 1.1 -
JSR 380 : Bean Validation 2.0
Hibernate-Validator
Hibernate Validator
是 Bean Validation
的參考實(shí)現(xiàn) . Hibernate Validator
提供了 JSR 303
規(guī)范中所有內(nèi)置 constraint 的實(shí)現(xiàn),除此之外還有一些附加的 constraint
。
代碼實(shí)現(xiàn)
-
如果項(xiàng)目的框架是 spring boot 的話,在 spring-boot-starter-web 中已經(jīng)包含了 Hibernate-validator 的依賴( 版本必須是2.3之前)。 2.3
以后的版本spring-boot-starter-web
已經(jīng)去除了這個(gè)依賴,需要手動引入Hibernate-validator
依賴,詳細(xì)內(nèi)容見官網(wǎng)描述
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
-
非 springboot
項(xiàng)目的話直接引入
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
代碼演示:方法前面這個(gè)注解@Valid
是必須的,否則不生效哦。
@PostMapping(value = "/save2")
@ResponseBody
public ResultViewModel save2(@Valid @RequestBody User user){
boolean saveUser = saveUser(user);
if (saveUser) {
return ResultViewModelUtil.success();
}
return ResultViewModelUtil.error();
}
實(shí)體類上標(biāo)上需要校驗(yàn)的規(guī)則注解就好了。
//被注釋的元素,值必須是一個(gè)字符串,不能為null,且調(diào)用trim()后,長度必須大于0
@NotBlank(message = "")
//被注釋的元素,值不能為null,但可以為"空",用于基本數(shù)據(jù)類型的非空校驗(yàn)上,而且被其標(biāo)注的字段可以使用 @size、@Max、@Min 等對字段數(shù)值進(jìn)行大小的控制
@NotNull(message = "")
//被注釋的的元素,值不能為null,且長度必須大于0,一般用在集合類上面
@NotEmpty(message = "")
//被注釋的元素必須符合指定的正則表達(dá)式。
@Pattern(regexp = "", message = "")
//被注釋的元素的大小必須在指定的范圍內(nèi)。
@Size(min =, max =)
//被注釋的元素,值必須是一個(gè)數(shù)字,且值必須大于等于指定的最小值
@Min(value = long以內(nèi)的值, message = "")
//被注釋的元素,值必須是一個(gè)數(shù)字,且值必須小于等于指定的最大值
@Max(value = long以內(nèi)的值, message = "")
//被注釋的元素,值必須是一個(gè)數(shù)字,其值必須大于等于指定的最小值
@DecimalMin(value = 可以是小數(shù), message = "")
//被注釋的元素,值必須是一個(gè)數(shù)字,其值必須小于等于指定的最大值
@DecimalMax(value = 可以是小數(shù), message = "")
//被注釋的元素,值必須為null
@Null(message = "")
//被注釋的元素必須是一個(gè)數(shù)字,其值必須在可接受的范圍內(nèi)
@Digits(integer =, fraction =)
//被注釋的元素,值必須為true
@AssertTrue(message = "")
//被注釋的元素,值必須為false
@AssertFalse(message = "")
//被注釋的元素必須是一個(gè)過去的日期
@Past(message = "")
//被注釋的元素必須是一個(gè)將來的日期
@Future(message = "")
//被注釋的元素必須是電子郵件地址
@Email(regexp = "", message = "")
//被注釋的元素必須在合適的范圍內(nèi)
@Range(min =, max =, message = "")
//被注釋的字符串的大小必須在指定的范圍內(nèi)
@Length(min =, max =, message = "")
唯一需要注意的點(diǎn)就是如果是級聯(lián)校驗(yàn)的話需要在最外層加上@Valid
為什么需要在校驗(yàn)的上一次標(biāo)上@Valid
這個(gè)注解,里面的校驗(yàn)才會生效列?有知道的或者感興趣的可以去看看源碼給我留言哦。然后在配置一個(gè)全局的異常捕獲器就好了,由于篇幅原因代碼就不貼了,代碼上傳到了
github
上。校驗(yàn)結(jié)果:
總結(jié)
-
Hibernate-Validator
還可以自定義注解實(shí)現(xiàn)。 -
還可以分組校驗(yàn)(有這樣一種場景,新增用戶信息的時(shí)候,不需要驗(yàn)證 userId
(因?yàn)橄到y(tǒng)生成);修改的時(shí)候需要驗(yàn)證userId
,這時(shí)候可用用戶到validator
的分組驗(yàn)證功能) -
如果項(xiàng)目不是 springboot
的、比如使用的是Jfinal框架(這個(gè)是個(gè)國產(chǎn)框架大多數(shù)人能都不知道)、或者soa
調(diào)用參數(shù)校驗(yàn)的時(shí)候,這時(shí)候可以怎么使用列? -
更多使用姿勢大家感興趣的可以去官網(wǎng)解鎖哦,不過日常開發(fā)的以上介紹基本就可以滿足需求了。 -
二胖看到這豐富的 api
,以及炒雞簡單的用法,趕緊把自己寫的輪子給刪除了,立馬換上了這個(gè)Hibernate-Validator
框架。重新修改提交后,leader
的臉上終于露出了滿意的笑容。 -
項(xiàng)目地址:https://github.com/worit1/validator
參考:
http://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html_single/#validator-gettingstarted(官網(wǎng)中文版本貼心吧
) https://docs.jboss.org/hibernate/validator/6.1/reference/en-US/html_single/#validator-gettingstarted https://juejin.im/post/5dd8d44c518825734e4cda22 https://www.cnblogs.com/mr-yang-localhost/p/7812038.html
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!