程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> SpringMVC集成Bean Validation 1.1

SpringMVC集成Bean Validation 1.1

編輯:JAVA綜合教程

SpringMVC集成Bean Validation 1.1


1、集成Bean Validation 1.1到SpringMVC

1.1、項目搭建

所需的jar包:

hibernate-validator-5.2.4.Final.jar

validation-api-1.0.0.GA.jar

slf4j-api-1.7.5.jar

首先添加hibernate validator 5依賴:

org.hibernatehibernate-validator5.0.2.Final

如果想在消息中使用EL表達式,請確保EL表達式版本是2.2或以上,如使用Tomcat6,請到Tomcat7中拷貝相應的EL jar包到Tomcat6中。

javax.eljavax.el-api2.2.4provided

請確保您使用的Web容器有相應版本的el jar包。

對於其他POM依賴請下載附件中的項目參考。

1.2、Spring MVC配置文件(spring-mvc.xml):

  classpath:messagesclasspath:org/hibernate/validator/ValidationMessages

此處主要把bean validation的消息查找委托給spring的messageSource。

1.3、實體驗證注解:

  publicclassUserimplementsSerializable{@NotNull(message="{user.id.null}")privateLongid;@NotEmpty(message="{user.name.null}")@Length(min=5,max=20,message="{user.name.length.illegal}")@Pattern(regexp="[a-zA-Z]{5,20}",message="{user.name.illegal}")privateStringname;@NotNull(message="{user.password.null}")privateStringpassword;}

對於驗證規則可以參考官方文檔,或者《第七章 注解式控制器的數據驗證、類型轉換及格式化》。

1.4、錯誤消息文件messages.properties:

user.id.null=用戶編號不能為空user.name.null=用戶名不能為空user.name.length.illegal=用戶名長度必須在5到20之間user.name.illegal=用戶名必須是字母user.password.null=密碼不能為空

1.5、控制器

@ControllerpublicclassUserController{@RequestMapping("/save")publicStringsave(@ValidUseruser,BindingResultresult){if(result.hasErrors()){return"error";}return"success";}}

1.6、錯誤頁面:

0}">字段錯誤:
${error.field}------${message}
0}">全局錯誤:
${message}

大家以後可以根據這個做通用的錯誤消息顯示規則。比如我前端頁面使用validationEngine顯示錯誤消息,那麼我可以定義一個tag來通用化錯誤消息的顯示:showFieldError.tag。

1.7、測試

輸入如:http://localhost:9080/spring4/save?name=123, 我們得到如下錯誤:

name------用戶名必須是字母name------用戶名長度必須在5到20之間password------密碼不能為空id------用戶編號不能為空

基本的集成就完成了。

如上測試有幾個小問題:

1、錯誤消息順序,大家可以看到name的錯誤消息順序不是按照書寫順序的,即不確定;

2、我想顯示如:用戶名【zhangsan】必須在5到20之間;其中我們想動態顯示:用戶名、min,max;而不是寫死了;

3、我想在修改的時候只驗證用戶名,其他的不驗證怎麼辦。

接下來我們挨著試試吧。

2、分組驗證及分組順序

如果我們想在新增的情況驗證id和name,而修改的情況驗證name和password,怎麼辦? 那麼就需要分組了。

首先定義分組接口:

publicinterfaceFirst{}publicinterfaceSecond{}

分組接口就是兩個普通的接口,用於標識,類似於java.io.Serializable。

接著我們使用分組接口標識實體:

publicclassUserimplementsSerializable{@NotNull(message="{user.id.null}",groups={First.class})privateLongid;@Length(min=5,max=20,message="{user.name.length.illegal}",groups={Second.class})@Pattern(regexp="[a-zA-Z]{5,20}",message="{user.name.illegal}",groups={Second.class})privateStringname;@NotNull(message="{user.password.null}",groups={First.class,Second.class})privateStringpassword;}

驗證時使用如:

@RequestMapping("/save")publicStringsave(@Validated({Second.class})Useruser,BindingResultresult){if(result.hasErrors()){return"error";}return"success";}

即通過@Validate注解標識要驗證的分組;如果要驗證兩個的話,可以這樣@Validated({First.class, Second.class})。

接下來我們來看看通過分組來指定順序;還記得之前的錯誤消息嗎? user.name會顯示兩個錯誤消息,而且順序不確定;如果我們先驗證一個消息;如果不通過再驗證另一個怎麼辦?可以通過@GroupSequence指定分組驗證順序:

@GroupSequence({First.class,Second.class,User.class})publicclassUserimplementsSerializable{privateLongid;@Length(min=5,max=20,message="{user.name.length.illegal}",groups={First.class})@Pattern(regexp="[a-zA-Z]{5,20}",message="{user.name.illegal}",groups={Second.class})privateStringname;privateStringpassword;}

通過@GroupSequence指定驗證順序:先驗證First分組,如果有錯誤立即返回而不會驗證Second分組,接著如果First分組驗證通過了,那麼才去驗證Second分組,最後指定User.class表示那些沒有分組的在最後。這樣我們就可以實現按順序驗證分組了。

另一個比較常見的就是級聯驗證:

如:

publicclassUser{@Valid@ConvertGroup(from=First.class,to=Second.class)privateOrganizationo;}

1、級聯驗證只要在相應的字段上加@Valid即可,會進行級聯驗證;@ConvertGroup的作用是當驗證o的分組是First時,那麼驗證o的分組是Second,即分組驗證的轉換。

3、消息中使用EL表達式

假設我們需要顯示如:用戶名[NAME]長度必須在[MIN]到[MAX]之間,此處大家可以看到,我們不想把一些數據寫死,如NAME、MIN、MAX;此時我們可以使用EL表達式。

如:

@Length(min=5,max=20,message="{user.name.length.illegal}",groups={First.class})

錯誤消息:

user.name.length.illegal=用戶名長度必須在{min}到{max}之間

其中我們可以使用{驗證注解的屬性}得到這些值;如{min}得到@Length中的min值;其他的也是類似的。

到此,我們還是無法得到出錯的那個輸入值,如name=zhangsan。此時就需要EL表達式的支持,首先確定引入EL jar包且版本正確。然後使用如:

user.name.length.illegal=用戶名[${validatedValue}]長度必須在5到20之間

使用如EL表達式:${validatedValue}得到輸入的值,如zhangsan。當然我們還可以使用如${min > 1 ? '大於1' : '小於等於1'},及在EL表達式中也能拿到如@Length的min等數據。

另外我們還可以拿到一個java.util.Formatter類型的formatter變量進行格式化:

${formatter.format("%04d",min)}

4、方法參數/返回值驗證

這個可以參考《Spring3.1 對Bean Validation規范的新支持(方法級別驗證)》,概念是類似的,具體可以參考Bean Validation 文檔。

5、自定義驗證規則

有時候默認的規則可能還不夠,有時候還需要自定義規則,比如屏蔽關鍵詞驗證是非常常見的一個功能,比如在發帖時帖子中不允許出現admin等關鍵詞。

1、定義驗證注解

packagecom.sishuok.spring4.validator;importjavax.validation.Constraint;importjavax.validation.Payload;importjava.lang.annotation.Documented;importjava.lang.annotation.Retention;importjava.lang.annotation.Target;importstaticjava.lang.annotation.ElementType.*;importstaticjava.lang.annotation.RetentionPolicy.*;/***

User:ZhangKaitao*

Date:13-12-15*

Version:1.0*/@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})@Retention(RUNTIME)//指定驗證器@Constraint(validatedBy=ForbiddenValidator.class)@Documentedpublic@interfaceForbidden{//默認錯誤消息Stringmessage()default"{forbidden.word}";//分組Class[]groups()default{};//負載Class[]payload()default{};//指定多個時使用@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})@Retention(RUNTIME)@Documented@interfaceList{Forbidden[]value();}}

2、 定義驗證器

packagecom.sishuok.spring4.validator;importorg.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.ApplicationContext;importorg.springframework.util.StringUtils;importjavax.validation.ConstraintValidator;importjavax.validation.ConstraintValidatorContext;importjava.io.Serializable;/***

User:ZhangKaitao*

Date:13-12-15*

Version:1.0*/publicclassForbiddenValidatorimplementsConstraintValidator{privateString[]forbiddenWords={"admin"};@Overridepublicvoidinitialize(ForbiddenconstraintAnnotation){//初始化,得到注解數據}@OverridepublicbooleanisValid(Stringvalue,ConstraintValidatorContextcontext){if(StringUtils.isEmpty(value)){returntrue;}for(Stringword:forbiddenWords){if(value.contains(word)){returnfalse;//驗證失敗}}returntrue;}}

驗證器中可以使用spring的依賴注入,如注入:@Autowired private ApplicationContext ctx;

3、使用

publicclassUserimplementsSerializable{@Forbidden()privateStringname;}

4、當我們在提交name中含有admin的時候會輸出錯誤消息:

forbidden.word=您輸入的數據中有非法關鍵詞

問題來了,哪個詞是非法的呢?bean validation 和 hibernate validator都沒有提供相應的api提供這個數據,怎麼辦呢?通過跟蹤代碼,發現一種不是特別好的方法:我們可以覆蓋org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl實現(即復制一份代碼放到我們的src中),然後覆蓋buildAnnotationParameterMap方法;

privateMapbuildAnnotationParameterMap(Annotationannotation){……//將Collections.unmodifiableMap(parameters);替換為如下語句returnparameters;}

即允許這個數據可以修改;然後在ForbiddenValidator中:

  for(Stringword:forbiddenWords){if(value.contains(word)){((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word",word);returnfalse;//驗證失敗}}

通過((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);添加自己的屬性;放到attributes中的數據可以通過${}獲取。然後消息就可以變成:

  forbidden.word=您輸入的數據中有非法關鍵詞【{word}】

這種方式不是很友好,但是可以解決我們的問題。

典型的如密碼、確認密碼的場景,非常常用;如果沒有這個功能我們需要自己寫代碼來完成;而且經常重復自己。接下來看看bean validation 1.1如何實現的。

6、類級別驗證器

6.1、定義驗證注解

  packagecom.sishuok.spring4.validator;importjavax.validation.Constraint;importjavax.validation.Payload;importjavax.validation.constraints.NotNull;importjava.lang.annotation.Documented;importjava.lang.annotation.Retention;importjava.lang.annotation.Target;importstaticjava.lang.annotation.ElementType.*;importstaticjava.lang.annotation.RetentionPolicy.*;/***

User:ZhangKaitao*

Date:13-12-15*

Version:1.0*/@Target({TYPE,ANNOTATION_TYPE})@Retention(RUNTIME)//指定驗證器@Constraint(validatedBy=CheckPasswordValidator.class)@Documentedpublic@interfaceCheckPassword{//默認錯誤消息Stringmessage()default"";//分組Class[]groups()default{};//負載Class[]payload()default{};//指定多個時使用@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})@Retention(RUNTIME)@Documented@interfaceList{CheckPassword[]value();}}

6.2、 定義驗證器

Java代碼 收藏代碼 packagecom.sishuok.spring4.validator;importcom.sishuok.spring4.entity.User;importorg.springframework.util.StringUtils;importjavax.validation.ConstraintValidator;importjavax.validation.ConstraintValidatorContext;/***

User:ZhangKaitao*

Date:13-12-15*

Version:1.0*/publicclassCheckPasswordValidatorimplementsConstraintValidator{@Overridepublicvoidinitialize(CheckPasswordconstraintAnnotation){}@OverridepublicbooleanisValid(Useruser,ConstraintValidatorContextcontext){if(user==null){returntrue;}//沒有填密碼if(!StringUtils.hasText(user.getPassword())){context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("{password.null}").addPropertyNode("password").addConstraintViolation();returnfalse;}if(!StringUtils.hasText(user.getConfirmation())){context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("{password.confirmation.null}").addPropertyNode("confirmation").addConstraintViolation();returnfalse;}//兩次密碼不一樣if(!user.getPassword().trim().equals(user.getConfirmation().trim())){context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("{password.confirmation.error}").addPropertyNode("confirmation").addConstraintViolation();returnfalse;}returntrue;}}

其中我們通過disableDefaultConstraintViolation禁用默認的約束;然後通過buildConstraintViolationWithTemplate(消息模板)/addPropertyNode(所屬屬性)/addConstraintViolation定義我們自己的約束。

 

6.3、使用

Java代碼 收藏代碼 @CheckPassword()publicclassUserimplementsSerializable{}

放到類頭上即可。

 

7、通過腳本驗證

Java代碼 收藏代碼 @ScriptAssert(script="_this.password==_this.confirmation",lang="javascript",alias="_this",message="{password.confirmation.error}")publicclassUserimplementsSerializable{}

通過腳本驗證是非常簡單而且強大的,lang指定腳本語言(請參考javax.script.ScriptEngineManager JSR-223),alias是在腳本驗證中User對象的名字,但是大家會發現一個問題:錯誤消息怎麼顯示呢? 在springmvc 中會添加到全局錯誤消息中,這肯定不是我們想要的,我們改造下吧。

 

7.1、定義驗證注解

packagecom.sishuok.spring4.validator;importorg.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator;importjava.lang.annotation.Documented;importjava.lang.annotation.Retention;importjava.lang.annotation.Target;importjavax.validation.Constraint;importjavax.validation.Payload;importstaticjava.lang.annotation.ElementType.TYPE;importstaticjava.lang.annotation.RetentionPolicy.RUNTIME;@Target({TYPE})@Retention(RUNTIME)@Constraint(validatedBy={PropertyScriptAssertValidator.class})@Documentedpublic@interfacePropertyScriptAssert{Stringmessage()default"{org.hibernate.validator.constraints.ScriptAssert.message}";Class[]groups()default{};Class[]payload()default{};Stringlang();Stringscript();Stringalias()default"_this";Stringproperty();@Target({TYPE})@Retention(RUNTIME)@Documentedpublic@interfaceList{PropertyScriptAssert[]value();}}

和ScriptAssert沒什麼區別,只是多了個property用來指定出錯後給實體的哪個屬性。

7.2、驗證器

packagecom.sishuok.spring4.validator;importjavax.script.ScriptException;importjavax.validation.ConstraintDeclarationException;importjavax.validation.ConstraintValidator;importjavax.validation.ConstraintValidatorContext;importcom.sishuok.spring4.validator.PropertyScriptAssert;importorg.hibernate.validator.constraints.ScriptAssert;importorg.hibernate.validator.internal.util.Contracts;importorg.hibernate.validator.internal.util.logging.Log;importorg.hibernate.validator.internal.util.logging.LoggerFactory;importorg.hibernate.validator.internal.util.scriptengine.ScriptEvaluator;importorg.hibernate.validator.internal.util.scriptengine.ScriptEvaluatorFactory;importstaticorg.hibernate.validator.internal.util.logging.Messages.MESSAGES;publicclassPropertyScriptAssertValidatorimplementsConstraintValidator{privatestaticfinalLoglog=LoggerFactory.make();privateStringscript;privateStringlanguageName;privateStringalias;privateStringproperty;privateStringmessage;publicvoidinitialize(PropertyScriptAssertconstraintAnnotation){validateParameters(constraintAnnotation);this.script=constraintAnnotation.script();this.languageName=constraintAnnotation.lang();this.alias=constraintAnnotation.alias();this.property=constraintAnnotation.property();this.message=constraintAnnotation.message();}publicbooleanisValid(Objectvalue,ConstraintValidatorContextconstraintValidatorContext){ObjectevaluationResult;ScriptEvaluatorscriptEvaluator;try{ScriptEvaluatorFactoryevaluatorFactory=ScriptEvaluatorFactory.getInstance();scriptEvaluator=evaluatorFactory.getScriptEvaluatorByLanguageName(languageName);}catch(ScriptExceptione){thrownewConstraintDeclarationException(e);}try{evaluationResult=scriptEvaluator.evaluate(script,value,alias);}catch(ScriptExceptione){throwlog.getErrorDuringScriptExecutionException(script,e);}if(evaluationResult==null){throwlog.getScriptMustReturnTrueOrFalseException(script);}if(!(evaluationResultinstanceofBoolean)){throwlog.getScriptMustReturnTrueOrFalseException(script,evaluationResult,evaluationResult.getClass().getCanonicalName());}if(Boolean.FALSE.equals(evaluationResult)){constraintValidatorContext.disableDefaultConstraintViolation();constraintValidatorContext.buildConstraintViolationWithTemplate(message).addPropertyNode(property).addConstraintViolation();}returnBoolean.TRUE.equals(evaluationResult);}privatevoidvalidateParameters(PropertyScriptAssertconstraintAnnotation){Contracts.assertNotEmpty(constraintAnnotation.script(),MESSAGES.parameterMustNotBeEmpty("script"));Contracts.assertNotEmpty(constraintAnnotation.lang(),MESSAGES.parameterMustNotBeEmpty("lang"));Contracts.assertNotEmpty(constraintAnnotation.alias(),MESSAGES.parameterMustNotBeEmpty("alias"));Contracts.assertNotEmpty(constraintAnnotation.property(),MESSAGES.parameterMustNotBeEmpty("property"));Contracts.assertNotEmpty(constraintAnnotation.message(),MESSAGES.parameterMustNotBeEmpty("message"));}}

和之前的類級別驗證器類似,就不多解釋了,其他代碼全部拷貝自org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator。

7.3、使用

@PropertyScriptAssert(property="confirmation",script="_this.password==_this.confirmation",lang="javascript",alias="_this",message="{password.confirmation.error}")

和之前的區別就是多了個property,用來指定出錯時給哪個字段。 這個相對之前的類級別驗證器更通用一點。

8、cross-parameter,跨參數驗證

直接看示例;

8.1、首先注冊MethodValidationPostProcessor,起作用請參考《Spring3.1 對Bean Validation規范的新支持(方法級別驗證)》

8.2、Service

@Validated@ServicepublicclassUserService{@CrossParameterpublicvoidchangePassword(Stringpassword,Stringconfirmation){}}

通過@Validated注解UserService表示該類中有需要進行方法參數/返回值驗證;@CrossParameter注解方法表示要進行跨參數驗證;即驗證password和confirmation是否相等。

8.3、驗證注解

packagecom.sishuok.spring4.validator;//省略import@Constraint(validatedBy=CrossParameterValidator.class)@Target({METHOD,CONSTRUCTOR,ANNOTATION_TYPE})@Retention(RUNTIME)@Documentedpublic@interfaceCrossParameter{Stringmessage()default"{password.confirmation.error}";Class[]groups()default{};Class[]payload()default{};}

8.4、驗證器

packagecom.sishuok.spring4.validator;//省略import@SupportedValidationTarget(ValidationTarget.PARAMETERS)publicclassCrossParameterValidatorimplementsConstraintValidator{@Overridepublicvoidinitialize(CrossParameterconstraintAnnotation){}@OverridepublicbooleanisValid(Object[]value,ConstraintValidatorContextcontext){if(value==null||value.length!=2){thrownewIllegalArgumentException("musthavetwoargs");}if(value[0]==null||value[1]==null){returntrue;}if(value[0].equals(value[1])){returntrue;}returnfalse;}}

其中@SupportedValidationTarget(ValidationTarget.PARAMETERS)表示驗證參數; value將是參數列表。

8.5、使用

@RequestMapping("/changePassword")publicStringchangePassword(@RequestParam("password")Stringpassword,@RequestParam("confirmation")Stringconfirmation,Modelmodel){try{userService.changePassword(password,confirmation);}catch(ConstraintViolationExceptione){for(ConstraintViolationviolation:e.getConstraintViolations()){System.out.println(violation.getMessage());}}return"success";}

調用userService.changePassword方法,如果驗證失敗將拋出ConstraintViolationException異常,然後得到ConstraintViolation,調用getMessage即可得到錯誤消息;然後到前台顯示即可。

從以上來看,不如之前的使用方便,需要自己對錯誤消息進行處理。 下一節我們也寫個腳本方式的跨參數驗證器。

9、混合類級別驗證器和跨參數驗證器

9.1、驗證注解

  packagecom.sishuok.spring4.validator;//省略import@Constraint(validatedBy={CrossParameterScriptAssertClassValidator.class,CrossParameterScriptAssertParameterValidator.class})@Target({TYPE,FIELD,PARAMETER,METHOD,CONSTRUCTOR,ANNOTATION_TYPE})@Retention(RUNTIME)@Documentedpublic@interfaceCrossParameterScriptAssert{Stringmessage()default"error";Class[]groups()default{};Class[]payload()default{};Stringscript();Stringlang();Stringalias()default"_this";Stringproperty()default"";ConstraintTargetvalidationAppliesTo()defaultConstraintTarget.IMPLICIT;}

此處我們通過@Constraint指定了兩個驗證器,一個類級別的,一個跨參數的。validationAppliesTo指定為ConstraintTarget.IMPLICIT,表示隱式自動判斷。

9.2、驗證器

請下載源碼查看

9.3、使用

9.3.1、類級別使用

@CrossParameterScriptAssert(property="confirmation",script="_this.password==_this.confirmation",lang="javascript",alias="_this",message="{password.confirmation.error}")

指定property即可,其他和之前的一樣。

9.3.2、跨參數驗證

@CrossParameterScriptAssert(script="args[0]==args[1]",lang="javascript",alias="args",message="{password.confirmation.error}")publicvoidchangePassword(Stringpassword,Stringconfirmation){}

通過args[0]==args[1] 來判斷是否相等。

這樣,我們的驗證注解就自動適應兩種驗證規則了。

10、組合驗證注解

有時候,可能有好幾個注解需要一起使用,此時就可以使用組合驗證注解

@Target({FIELD})@Retention(RUNTIME)@Documented@NotNull(message="{user.name.null}")@Length(min=5,max=20,message="{user.name.length.illegal}")@Pattern(regexp="[a-zA-Z]{5,20}",message="{user.name.length.illegal}")@Constraint(validatedBy={})public@interfaceComposition{Stringmessage()default"";Class[]groups()default{};Class[]payload()default{};}

這樣我們驗證時只需要:

@Composition()privateStringname;

簡潔多了。

11、本地化

即根據不同的語言選擇不同的錯誤消息顯示。

1、本地化解析器

此處使用cookie存儲本地化信息,當然也可以選擇其他的,如Session存儲。

2、設置本地化信息的攔截器

即請求參數中通過language設置語言。

3、消息文件

\

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved