Play框架中Action的参数绑定和验证

5月 15th, 2012 2,965 留下评论 阅读评论

再来讲讲Play框架中Action的参数绑定,用过的都说好,哈哈。不仅可以将任意字符串值(比如用户名和密码)、日期值(生日)、整数值(第几页、年龄等)直接作为Action方法的参数放入其中自动绑定,还可以对继承自play.db.jpa.JAPBase的Model类进行直接的封装,这对一个具有很多变量的JavaBean来说实在是一件令人惊喜的事情,省略了太多太多重复又难看的代码有木有!

另外一个令人感觉兴奋的就是参数验证机制,再也不用自己针对Model的每一个属性值去写判断检验的代码,也不用像Struts那样要写复杂的xml文件去作验证,现在的框架十分流行Annotation,只要在Model类中给每个属性上一个类似@Required、@Email、@Phone、@MaxSize之类的注解,剩下的工作Play全部自动会完成了,你最后需要的只是给这个Model类传入Action方法时加一个@Valid注解或者在Action方法中调用validation.valid(obj)就OK了。

其实方法动态参数绑定这点在Spring MVC中也已经有了,但是一般只用来传入HttpServletRequest和HttpServletResponse。

当执行到play.mvc.ActionInvoker.invoke()时,在调用完resolve(request, response)后Play的路由工作已经完成,具体可参见Play框架的Router机制和jregex包一文。然后得到了需要调用的Action方法的Method类,在真正调用Action前,就需要执行完参数绑定的工作,如果参数带有@Valid或者@As注解,还要进行验证工作。代码继续执行:

	Play.pluginCollection.beforeActionInvocation(actionMethod);

这句话调用Play的插件机制,在调用Action方法前进行各种操作,算是一个Hook,这在Wordpress中非常常见。其中有一个插件play.data.validation.ValidationPlugin,它的工作在beforeActionInvocation(actionMethod)中会检查这个Action的所有参数是否带有任何注解,若有则在这里启动参数绑定和验证工作,若无则直接返回。

不管ValidationPlugin是否提前完成了工作,ActionInvoker.invoke()代码继续进行来到invokeControllerMethod(actionMethod),然后在里面继续进行参数绑定和验证工作。

前面无论是ValidationPlugin还是后来调用的invokeControllerMethod(actionMethod),真正进行参数绑定和验证工作的地方都在它们都会调用的

	ActionInvoker.getActionMethodArgs(Method method, Object o);

此函数先判断参数是否已经完成绑定工作(即之前是否已被ValidationPlugin调用过了),然后真正进入参数绑定和验证工作。

	Object[] rArgs = new Object[method.getParameterTypes().length];
	for (int i = 0; i < method.getParameterTypes().length; i++){
		Class type = method.getParameterTypes()[i];
		...
		rArgs[i] = Binder.bind(...);
	}

这个循环遍历Action Method中的每一个参数,然后通过play.data.binding.Binder类来执行具体的参数绑定工作。

	result = Play.pluginCollection.bind(parentParamNode, name, clazz, type, annotations);

首先这条语句再次遍历Play的插件集合来实现参数绑定,其中的play.db.jpa.JPAPlugin会跳出来完成参数是JPABase子类(一般为models包下的Model类)的参数绑定。这里会特判一下参数中是否带有id属性,若有则执行一条数据库查询获得此Model的这条数据的Object,若无则通过反射拿到无参构造重新new一个Object。然后就会调用play.db.jpa.GenericModel.edit()方法将参数绑定到这个Object中。在edit()方法中,通过反射获取JavaBean所有的Field(这边的Field若带有外键性质的注解,如@OneToOne,会触发递归绑定),绕了一圈最后调用Binder.internalBind()完成对每个Field的绑定。

如果参数不是JPABase的子类只是Java中的普通类,则直接调用Binder.internalBind()来搞定就好了。

	Binder.internalBind()

这个方法完成了对Java基本类型和一些常用的集合类、属性类的值绑定。

  1. 若Field是Enum或其子类,调用Binder.bindEnum(),由Enum.valueOf()实现
  2. 若Field是Map或其子类,调用Binder.bindMap()
  3. 若Field是Collection或其子类,调用Binder.bindCollection()
  4. 若Field是数组,调用Binder.bindArray()
  5. 都不是,则调用Binder.directBind()
  6. 在directBind中,会从Map<Class<?>, TypeBinder<?>> Binder.supportedTypes中寻找是否是Play指定的特殊的可以绑定的Java类,比如java.util下的Date、DateTime、Calendar或者java.io.File等,具体见Binder类的static初始化域
  7. 若也不是上面这些稍复杂的类型,那就是Java的基本类型了,按照String、Character、int或Integer、long或Long、double或Double、BigDecimal、boolean或Boolean这样的顺序依次解析和绑定
  8. 在internalBind中会catch住所有抛出的解析异常,从而调用Validation.addError()完成验证中添加错误信息的工作

这些类型的数据绑定中,若数据值为空,则相应地返回对应类型的默认值(0、0.0、false、new Object()等)。

至于参数验证,很简单地遍历每个Field根据其拥有的注解一个一个地去自动检查就好了,这没有难度,无论是@Valid注解还是手动调用validation.valid()方法都是差不多的。

的确还是很复杂,涉及到的代码也很多,所以当一个JavaBean的属性值不是太多时,还是建议一一获取属性值自己来组建Model类的方式比较好,一是效率更高,二是Html模板中也不用在input的name属性值中用model.field这种形式,感觉不是很好。

Categories: Java 标签:, ,
  1. 1月 16th, 2017 09:08

    “The unutrofnate fact is that the actions of a confused and frightened monkey are never that difficult to predict.” I almost peed myself from the mental image this created. Can you predict if Henry Rollins is ever going to call me?