springmvc的验证 注解验证 与参数格式无法转换时

spring 官方提供了一个例子

https://src.springframework.org/svn/spring-samples/mvc-basic/trunk

例子



@RequestMapping(value = { "/", "home.do", "index", "index.jsp", "index.html", "index.htm" })

public String home(@Valid @ModelAttribute("user") User user, BindingResult result,

ModelMap model) throws IOException {

if(result.hasErrors()) { //给当前对象(BindingResult 前面的参数就是当前对象,每一个参数和BindingResult 一一对应,他们之间必须紧跟着,否则会报错,如这里的result必须跟着user,而不能将result写在model之后)注册一个字段错误。字段名可以为null,表示整个对象错误)。
result.rejectValue(null, null, null, null); System.out.println();
}

List messages = new ArrayList(); 
messages.add("你没有登录,请先登录!"); 
messages.add("你输入的用户名不存在!");
messages.add("无效的账号");
model.put("messages", messages); 
return "application/index"; 
} 

public class User { 
private Integer id; 
private String userName; 
/** * * 返回 id 的值 * * @return id */ 
@NotNull(message = "user.id.notnull") 
public Integer getId() { return id; } 
/** * * 设置 id 的值 * * @param id */ 
public void setId(Integer id) { this.id = id; }
}


我在自己的一个测试应用中会报错 错误内容大概就是:
StandardWrapper.Throwable org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.validation.beanvalidation.LocalValidatorFactoryBean#0': Invocation of init method failed; nested exception is javax.validation.ValidationException: Unable to find a default provider

这是因为我只加了validation-api-1.0.0.GA.jar架包,但是hibernate-validator-4.1.0.Final.jar架包没有加入
(hibernate-validator的版本不一定必须是是4.1.0.Final)。
另外它还需要
log4j-1.2.16.jar
slf4j-api-1.6.1.jar
slf4j-log4j12-1.6.1.jar

在调用spring.ftl的showErrors宏会调用status.errorMessages,这里的errorMessages消息是怎么生成的(因为我的项目中要考虑到国际化问题,所以需要了解,看看怎么做国际化消息提醒)。

注解适配器,在初始化参数的时候,会进入 HandlerMethodInvoker类的resolveHandlerArguments(Method, Object, NativeWebRequest, ExtendedModelMap) 方法,它会根据方法的参数的注解,来初始化数据。 其中有一句是

else if ("Valid".equals(paramAnn.annotationType().getSimpleName())) { 
  validate = true; 
} 

来确定这个参数是否需要验证。

获取这个字段的
WebDataBinder WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler);


注意这个绑定器和WebBindingInitializer绑定器不是一回事。

绑定器有效,就调用绑定方法。

doBind(binder, webRequest, validate, !assignBindingResult);

绑定方法如下

private void doBind(WebDataBinder binder, NativeWebRequest webRequest, boolean validate, boolean failOnErrors) throws Exception { 
doBind(binder, webRequest); 
if (validate) { binder.validate(); } 
if (failOnErrors && binder.getBindingResult().hasErrors()) { throw new BindException(binder.getBindingResult()); }
} 

可以看出来,验证只和

if (validate) { binder.validate(); }

这句有关系。 我们只需跟踪这里。 验证方法是

public void validate() { //首先获取的是验证器 
Validator validator = getValidator(); 
if (validator != null) { //然后调用验证器的验证方法,这个时候我们使用的是LocalValidatorFactoryBean验证器,但它实际上则调用它的父类SpringValidatorAdapter的验证方法,因为LocalValidatorFactoryBean没实现验证方法 validator.validate(getTarget(), getBindingResult()); } 
} 


跟踪验证器的验证方法
public void validate(Object target, Errors errors) { //这部是调用jsr validator 的实现。不会影响spring mvc的运行结果。不做解释。这里由于我只在User模写的id属性设置的NotNull,所以只会有一个错误返回如图 
result = this.targetValidator.validate(target); 
for (ConstraintViolation violation : result) { //这部分当然是获取验证的出错的字段名 String field = violation.getPropertyPath().toString(); //将错误字段封装成一个fieldError,因为是第一次验证这个字段,所以是null 
FieldError fieldError = errors.getFieldError(field); 
if (fieldError == null || !fieldError.isBindingFailure()) { 
try { //violation.getConstraintDescriptor(),返回的javax.validation.metadata.ConstraintDescriptor对象包含了这个错误字段的注解等信息。以及注解中的值,其中annotationType的信息就是注解值信息(@NotNull(message="xxx")这个时候message就是他的字段值。未设置值的字段取它的注解里面的默认值)如图 getSimpleName方法一般返回的是注解类名,如果是多个注解,组合起来返回。这个时候就返回NotNull,这个时候NotNull就作为errors.rejectValue方法的code参数 getArgumentsForConstraint方法则是通过对象名(errors保存了当前验证的对象名,如我这里验证的是action中的方法的user参数对象的id,那么就是user),字段和验证描述信息,获取国际化参数列表值,作为rejectValue方法的第三个参数(args)。第四个参数是默认消息,将注解中的message属性为默认值。很可能从属性文件取消息的时候,所使用的code就是在这里面初始化好的。所以我这里跟踪进rejectValue方法。 errors.rejectValue(field, violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), getArgumentsForConstraint(errors.getObjectName(), field, violation.getConstraintDescriptor()), violation.getMessage()); } catch (NotReadablePropertyException ex) { throw new IllegalStateException("JSR-303 validated property '" + field + "' does not have a corresponding accessor for Spring data binding - " + "check your DataBinder's configuration (bean property versus direct field access)", ex); } } } } getArgumentsForConstraint方法 protected Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor> descriptor) { List arguments = new LinkedList(); //将对象和字段组合user.id,字段自己id组合成一个字符串数组。 String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; 创建DefaultMessageSourceResolvable,以字段名(这里是id)作为默认消息,这个是在消息参数化处理的,它会在第一个参数化值{0}的处理的时候,使用codes,类似的操作去调用messageSource的getMessage(codes, "id", locale),如果没有取到,就把fieldName作为值返回,如果取到,就用这个参数值覆盖,如果没有取到,就用fileName覆盖。这个时候当然先找user.id的message,没找到找id的message,没找到就直接使用id。 arguments.add(new DefaultMessageSourceResolvable(codes, field)); // Using a TreeMap for alphabetical ordering of attribute names Map attributesToExpose = new TreeMap(); //遍历注解中的属性 for (Map.Entry entry : descriptor.getAttributes().entrySet()) { String attributeName = entry.getKey(); Object attributeValue = entry.getValue(); //判断属性是否在internalAnnotationAttributes中。internalAnnotationAttributes是以开始就初始化好的,初始化的代码如下。如果不存在,就添加进这个玩意 /*private static final Set internalAnnotationAttributes = new HashSet(3); static { internalAnnotationAttributes.add("message"); internalAnnotationAttributes.add("groups"); internalAnnotationAttributes.add("payload"); }*/ if (!internalAnnotationAttributes.contains(attributeName)) { attributesToExpose.put(attributeName, attributeValue); } } //将所有的不存在于internalAnnotationAttributes里面的属性值添加进arguments,这会使得注解中的其他属性值最为属性文件里面的变量消息值了。。这个时候由于我没有增加更多的属性,所以是空。 arguments.addAll(attributesToExpose.values()); return arguments.toArray(new Object[arguments.size()]); } errors.rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage)方法 public void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage) { if ("".equals(getNestedPath()) && !StringUtils.hasLength(field)) { reject(errorCode, errorArgs, defaultMessage); return; } String fixedField = fixedField(field); Object newVal = getActualFieldValue(fixedField); //这里是初始化字段错误 FieldError fe = new FieldError( getObjectName(), fixedField, newVal, false, //resolveMessageCodes方法初始化codes,也就是这个字段的在属性文件中消息的key,因此大概就是在这里初始化好了这个字段所有的key,我首先的怀疑是codes包含了:{errorCode,errorCode+field}两种code(这里的errorCode的值是NotNull)。进入。 resolveMessageCodes(errorCode, field), errorArgs, defaultMessage); addError(fe); } //看来这个方法不简单,最后的codes数组不是简单的几个code,可能由多个code组合而成。继续跟踪getMessageCodesResolver方法,只是返回return this.messageCodesResolver;然后使用messageCodesResolver来生成codes,这里返回的MessageCodesResolver是org.springframework.validation.DefaultMessageCodesResolver,这个org.springframework.validation.DefaultMessageCodesResolver对象是在new BindResult对象的时候,在其AbstractBindingResult的类属性初始化好的。初始化代码是 private MessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); 可以看到,默认创建的MessageCodes对象是一个DefaultMessageCodesResolver,它没有设置任何属性(实际上它只有一个prefix属性) public String[] resolveMessageCodes(String errorCode, String field) { Class fieldType = getFieldType(field); return getMessageCodesResolver().resolveMessageCodes( errorCode, getObjectName(), fixedField(field), fieldType); } DefaultMessageCodesResolver的resolveMessageCodes方法 这里就是生成codes的实际方法了 public String[] resolveMessageCodes(String errorCode, String objectName, String field, Class fieldType) { List codeList = new ArrayList(); List fieldList = new ArrayList(); //这个方法的doc解释是Add both keyed and non-keyed entries for the supplied field to the supplied field list,我英文不咋的,求解释。不过暂时还用不上。 另外,静态常量CODE_SEPARATOR = "."; buildFieldList(field, fieldList); for (String fieldInList : fieldList) { //第一个code应该就是errorCode + CODE_SEPARATOR + objectName + CODE_SEPARATOR + fieldInList了,还是进去看看postProcessMessageCode方法就是一句,return getPrefix() + code;,因为我没设置前缀,所以这里的结果应该是NotNull.user.id codeList.add(postProcessMessageCode(errorCode + CODE_SEPARATOR + objectName + CODE_SEPARATOR + fieldInList)); } int dotIndex = field.lastIndexOf('.'); if (dotIndex != -1) { //将组合字段的最后一个字段名取出,如 user的name.firstName.bigChar.firstChar就是firstChar了。 buildFieldList(field.substring(dotIndex + 1), fieldList); } //这里我的结果应该是添加一个NotNull.id for (String fieldInList : fieldList) { codeList.add(postProcessMessageCode(errorCode + CODE_SEPARATOR + fieldInList)); } if (fieldType != null) { //这里我的结果就是NotNull.java.lang.Integer codeList.add(postProcessMessageCode(errorCode + CODE_SEPARATOR + fieldType.getName())); } //这里我的结果是NotNull 所以最后我的codes有四个,分别是 NotNull.user.id, NotNull.id, NotNull.java.lang.Integer, NotNull codeList.add(postProcessMessageCode(errorCode)); return StringUtils.toStringArray(codeList); } 下面就是我如果想在前面添加code的前缀,就需要设置BindingResult的MessageCodesResolver。而设置MessageCodesResolver是通过WebDataBinder的binder.setMessageCodesResolver(messageCodesResolver);设置的,他会调用getInternalBindingResult().setMessageCodesResolver(messageCodesResolver);来设置实际的BindResult来设置。如果我们要设置全局的前缀,只需写一个WebBindingInitializer,并设置到注解适配器的bean中就可以了。因为在WebBindingInitializer里面可以获取到WebDataBinder。而如果我们需要在每一个Controller类添加不同的前缀,只需要写一个@InitBinder注解过的方法就好了。 @InitBinder() public void initBinder(WebDataBinder binder) { DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver(); messageCodesResolver.setPrefix("user."); binder.setMessageCodesResolver(messageCodesResolver); } 在ftl中展示字段错误,是通过showErrors 来暂时字段错误的。 "/> 我们一般在showErrors 之前需要调用一次formInput ,才能让showErrors正常工作 因为formInput 会调用bind宏,来初始化这个字段对应的status ${error} #if> ${error} #if> ${separator}#if> #list> #macro> 而bind宏则是 #if> #if> #macro> 可以看出来,bind宏每次调用之后,都会重新定义status和stringStatusValue 所以才能让showErrors 正常工作。 首先status对应的类型是org.springframework.web.servlet.support.BindStatus类型。 进到它的 public String[] getErrorMessages() { initErrorMessages(); return this.errorMessages; } 方法,可以发现首先需要initErrorMessages初始化消息。 if (this.errorMessages == null) { this.errorMessages = new String[this.objectErrors.size()]; for (int i = 0; i (); } Errors errors = this.errorsMap.get(name); boolean put = false; if (errors == null) { //从这里可以看出,BindResult对象在model中,以org.springframework.validation.BindingResult.(MODEL_KEY_PREFIX 的值是org.springframework.validation.BindingResult.)+对象名方式命名,这里是org.springframework.validation.BindingResult.user errors = (Errors) getModelObject(BindingResult.MODEL_KEY_PREFIX + name); // Check old BindException prefix for backwards compatibility. if (errors instanceof BindException) { errors = ((BindException) errors).getBindingResult(); } if (errors == null) { return null; } put = true; } if (htmlEscape && !(errors instanceof EscapedErrors)) { errors = new EscapedErrors(errors); put = true; } else if (!htmlEscape && errors instanceof EscapedErrors) { errors = ((EscapedErrors) errors).getSource(); put = true; } if (put) { this.errorsMap.put(name, errors); } return errors; } codes大概内容是 NotNull.user.id(errorCode.+objectName.field)优先级 0 NotNull.id(errorCode.+fieldLastName)优先级 1 NotNull.java.lang.Integer(errorCode.+fieldType.getName())优先级 2 NotNull(errorCode)优先级 3 参数无法转换 以Date类型作为例子 当出现请求参数无法转换成Date类型。直接输出字段error,就会发现,输出的是异常的message。 调试发现它这个时候的codes是 [typeMismatch.activitySave.activityStartTime, typeMismatch.activityStartTime, typeMismatch.java.util.Date, typeMismatch]

猜你喜欢

转载自liyixing1.iteye.com/blog/1103706