继续之前讲的SSH+EJB迁移到Play 1.2.3上,今天出了一个很奇怪的BUG。
在进行表单提交的时候,提交数据中有一个List列表数据,如果List数据多的话,服务端处理会很慢,有时甚至超时!比如10条的时候,处理时间就多达50多秒~要命的问题,在调试中发现,业务逻辑方面没有问题,代码编写处没有问题。而问题出在Play框架对请求参数的绑定上。即反序列化List对象。
无奈只好有去啃源码……在源码中打入日志输出点后,发现List的元素出现重复设定。比如,如果该次请求List有十个元素,正常对列表List,应该只会调用set 10次。但是日志显示,大约有500次set,每个元素重复40多次……而该次请求的List元素类中有字段40多个,加上其他一些请求参数,正好500左右个K/V对。
也就是说play框架对请求参数中K/V参数对中,是该List的子孙元素都进行了一次set。
为了验证上面的思路,我们可以看看源码,为了缩小篇幅,只显示BUG
相关的代码: Play/data/binding/Binder.java#bindInternal()
public static Object bindInternal(String name, Class clazz, Type type, Annotation[] annotations, Map params, String suffix, String[] profiles) {
try {
Logger.trace("bindInternal: name [" + name + "] suffix [" + suffix + "]");
String[] value = params.get(name + suffix);
Logger.trace("bindInternal: value [" + value + "]");
Logger.trace("bindInternal: profile [" + Utils.join(profiles, ",") + "]");
if (annotations != null) {... }
// Arrays types
// The array condition is not so nice... We should find another way of doing this....
if (clazz.isArray() && (clazz != byte[].class && clazz != byte[][].class && clazz != File[].class && clazz != Upload[].class)) {...}
// Enums
if (Enum.class.isAssignableFrom(clazz)) {...}
// Map
if (Map.class.isAssignableFrom(clazz)) {...}
// Collections types
if (Collection.class.isAssignableFrom(clazz)) {
...
if (value == null) {
value = params.get(name + suffix + "[]");
if (value == null && r instanceof List) {
//@1 遍历所有的参数
for (String param : params.keySet()) {
Pattern p = Pattern.compile("^" + escape(name + suffix) + "\\[([0-9]+)\\](.*)$");
Matcher m = p.matcher(param);
if (m.matches()) {
int key = Integer.parseInt(m.group(1));
//@2 根据上面匹配到的index值,对list进行扩容
while (((List) r).size() <= key) {
((List) r).add(null);
}
if (isComposite(name + suffix + "[" + key + "]", params)) {
BeanWrapper beanWrapper = getBeanWrapper(componentClass);
//@3 跳转到BeanWrapper类,对key值所在的index位置的元素进行绑定。
Object oValue = beanWrapper.bind("", type, params, name + suffix + "[" + key + "]", annotations);
//@4 将绑定得到的结果set到list中
((List) r).set(key, oValue);
} else {
Map tP = new HashMap();
tP.put("value", params.get(name + suffix + "[" + key + "]"));
Object oValue = bindInternal("value", componentClass, componentClass, annotations, tP, "", value);
if (oValue != MISSING) {
((List) r).set(key, oValue);
}
}
}
}
return r.isEmpty() ? MISSING : r;
}
}
...
}
...
} catch (Exception e) {
Validation.addError(name + suffix, "validation.invalid");
return MISSING;
}
}
Play/data/binding/BeanWrapper.java#bind()
public Object bind(String name, Type type, Map params, String prefix, Object instance, Annotation[] annotations) throws Exception {
//@1 遍历要绑定的元素的类的每个字段
for (Property prop : wrappers.values()) {
Logger.trace("beanwrapper: prefix [" + prefix + "] prop.getName() [" + prop.getName() + "]");
for (String key : params.keySet()) {
Logger.trace("key: [" + key + "]");
}
String newPrefix = prefix + "." + prop.getName();
if (name.equals("") && prefix.equals("") && newPrefix.startsWith(".")) {
newPrefix = newPrefix.substring(1);
}
Logger.trace("beanwrapper: bind name [" + name + "] annotation [" + Utils.join(annotations, " ") + "]");
Object value = Binder.bindInternal(name, prop.getType(), prop.getGenericType(), prop.getAnnotations(), params, newPrefix, prop.profiles);
if (value != Binder.MISSING) {
if (value != Binder.NO_BINDING) {
prop.setValue(instance, value);
}
} else {
Logger.trace("beanwrapper: bind annotation [" + Utils.join(prop.getAnnotations(), " ") + "]");
//@2 调用bindInternal绑定元素的字段
value = Binder.bindInternal(name, prop.getType(), prop.getGenericType(), annotations, params, newPrefix, prop.profiles);
Logger.trace("beanwrapper: value [" + value + "]");
if (value != Binder.MISSING && value != Binder.NO_BINDING) {
//@3 set字段的值
prop.setValue(instance, value);
}
}
}
return instance;
}
可以看到两个类互相有引用,有点递归调用的意思,其逻辑大概如注释所说: Bind类中遍历所有的K/V,正则匹配到列表的index值,然后交给BeanWrapper去绑定这个index处的列表元素。
而BeanWrapper类则遍历传入的类的字段,调用Bind类中的方法(在Bind中又会扫描一次K/V),绑定结束后,将元素返回 Bind类中接受返回的结果,并set 入list。 待所有的K/V遍历结束之后。
该列表绑定处理即结束。 如果理解了这个过程之后,就好处理了,在每次set之前增加一个判断即可。因为第一次set一个列表元素时,已经扫描了K/V中该index相关的元素字段,重复设定没有意义。
代码修改就比较容易,在Binder.java中:
...
//@2 根据上面匹配到的index值,对list进行扩容
while (((List) r).size() <= key) {
((List) r).add(null);
}
//add to jump List element repeat bind hechaoyang@2014/10/14
if(((List) r).get(key) != null){
continue;
}
if (isComposite(name + suffix + "[" + key + "]", params)) {
BeanWrapper beanWrapper = getBeanWrapper(componentClass);
//@3 跳转到BeanWrapper类,对key值所在的index位置的元素进行绑定。
Object oValue = beanWrapper.bind("", type, params, name + suffix + "[" + key + "]", annotations);
//@4 将绑定得到的结果set到list中
((List) r).set(key, oValue);
} else {
...
为了验证效果,我们新建一个小工程,给了一个类型简单的类:
package models;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Mity {
@Id
public int id;
public String name1;
...
public String name39;
}
这里只给出Model的代码,是为了下面的效果。如想自测,请自行补充其他代码。 同样的10条测试数据在没有修改之前:
修改之后:
显然不是一个数量级别的,这个问题我在1.2.7和1.3.0上测试已经不存在了。可能是已经优化过了。