?
一、来由
??? ? 最近一段时间使用webwork比较多,在使用上有一些想法,比如表单校验,action的使用,webwork的URL格式等等。本次把表单这方面的想法和做法简单总结一下。
????? 我先把系统结构简单表述一下:
??????????? webwork 2. 2.5 + spring 2.0 + velocity 1.4 + ibatis2.3.4
?
一、webwork的表单校验
????? 使用webwork的action在编写相应的post处理的时候,可以通过在action的相应方法或者validate方法中进行硬编码校验,或者通过对应的表单校验配置文件 ActionName-validation.xml 或 ActionName-alias-validation.xml等来处理。这样的处理在Action只处理一件事情的时候还比较简单,但是如果Action中包含了n个doXXX(),那么使用起来就比较别扭。
?????? 而且我始终认为ActionName-validation.xml放在Action同一个路径下是一个很差的设计。那是不是可以同时符合两个条件:
?????? A. Action 可使用多个doXxx()
?????? B. 针对每个doXxx()进行表单校验
????? 看了webwork的validation的实现,我们可以新增一下validation替换webwork的validation。
?
三、改进想法
????? 新增一个Validation拦截器和表单验证文件(可多个),在执行AxtionName.doXxx()方法时,根据对应action.doXxx()方法上的一个注释(比如@Form(group="login"))来查找对应的表单验证配置,然后执行校验。
对于表单的详细校验方法还是webwork本身的校验方法,只是把查找表单和表单配置文件的格式调整一下。
???? 那么需要做下面几件事情:
????? A. 定义表单配置文件格式。
????? B. 编写Validation拦截器:ValidationInterceptor
????? C. 编写表单校验文件加载处理,FormResolver 及实现 DefaultFormResolver
????? D. 编写annotation类:Form 和 表单描述类 Group
四、定义表单文件
????? 文件格式用dtd表述如下:
?
<?xml version="1.0" encoding="UTF-8"?> <!ELEMENT forms (group)+> <!ELEMENT group (field | validator)+> <!ATTLIST group name CDATA #REQUIRED > <!ELEMENT field (field-validator+)> <!ATTLIST field name CDATA #REQUIRED > <!ELEMENT field-validator (param*, message)> <!ATTLIST field-validator type CDATA #REQUIRED short-circuit (true | false) "false" > <!ELEMENT validator (param*, message)> <!ATTLIST validator type CDATA #REQUIRED short-circuit (true | false) "false" > <!ELEMENT param (#PCDATA)> <!ATTLIST param name CDATA #REQUIRED > <!ELEMENT message (#PCDATA)> <!ATTLIST message key CDATA #IMPLIED >
? ? 用树形表述如下:
?
forms | |----group(*) | |-----field | | | |-- field-validator |-----validator
??? 其实就是把webwork的actionName-validation.xml文件中的配置放入到group中,并给group设置一个属性name,在外层嵌套一个forms。
??? 那么就可以把所有的表单验证文件放入到一个配置文件,或者多个配置文件中。例子如下:
?
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE form PUBLIC "-//alisoft software//DTD eshop webwork Validator 1.0//EN" "http://www.alisoft.com/dtd/eshop-validator-1.0.dtd"> <forms> <group name="login"> <field name="user.loginId"> <field-validator type="email"> <message>必须为合法的Email格式</message> </field-validator> </field> <field name="user.password"> <field-validator type="requiredstring"> <message>密码不能为空</message> </field-validator> <field-validator type="stringlength"> <param name="minLength">4</param> <param name="maxLength">12</param> <message>长度必须在${minLength}和${maxLength}之间,当前的长度为${user.password.length()} </message> </field-validator> </field> </group> </forms>
?
五、编写Validation拦截器:ValidationInterceptor
? ?? 代码如下:
?
import java.lang.annotation.Annotation; import java.lang.reflect.Method; import com.opensymphony.xwork.ActionInvocation; import com.opensymphony.xwork.interceptor.MethodFilterInterceptor; import com.xbuy.eshop.framework.util.BeanFactoryUtil; import com.xbuy.eshop.framework.validator.form.Form; import com.xbuy.eshop.framework.validator.form.FormResolver; import com.xbuy.eshop.framework.validator.form.Group; import freemarker.template.utility.ClassUtil; @SuppressWarnings("serial") public class ValidationInterceptor extends MethodFilterInterceptor { protected void doBeforeInvocation(ActionInvocation invocation) throws Exception { Object action = invocation.getAction(); String method = invocation.getProxy().getMethod(); Form form = getForm(invocation, method); //有设置注释的进行validate if (form != null) { String groupName = form.group(); ? ? ? ? ? ? FormResolver formResolver = (FormResolver) BeanFactoryUtil .getBean(FormResolver.BEAN_NAME); ? ? ? ? ? ? ?if (formResolver == null) { log.error("Validating error:not found fromResolver!"); return; } Group group = formResolver.fetchGroup(groupName); if (group == null) { log.error("Validating error:not found name='" + groupName + "'s group !"); return; } group.validate(action); } if (log.isDebugEnabled()) { log.debug("Validating " + invocation.getProxy().getNamespace() + "/" + invocation.getProxy().getActionName() + " with method " + invocation.getProxy().getMethod() + "."); } } @SuppressWarnings("unchecked") private Form getForm(ActionInvocation invocation, String method) throws ClassNotFoundException { Class cls = ClassUtil.forName(invocation.getProxy().getConfig().getClassName()); Method[] methods = cls.getMethods(); if (methods != null) { Method currentMethod = null; for (Method ms : methods) { if (method.equalsIgnoreCase(ms.getName())) { currentMethod = ms; break; } } if (currentMethod != null) { Annotation[] anns = currentMethod.getAnnotations(); if (anns != null) { Annotation currentAnn = null; for (Annotation ann : anns) { if (ann.annotationType().equals(Form.class)) { currentAnn = ann; break; } } return (Form) currentAnn; } } } return null; } protected String doIntercept(ActionInvocation invocation) throws Exception { doBeforeInvocation(invocation); return invocation.invoke(); } }
?? 其中:
FormResolver formResolver = (FormResolver) BeanFactoryUtil.getBean(FormResolver.BEAN_NAME);???
?? 表示从Spring BeanFactory中获取对应的Bean: ?? formResolver
?
六、编写表单校验文件加载处理,FormResolver 及实现 DefaultFormResolver ? ???
???? FormResolver如下:
?
public interface FormResolver { public static final String BEAN_NAME = "formResolver"; /** * 获取group定义 * * @param groupName * @return */ Group fetchGroup(String groupName); }
? ? DefaultFormResolver如下:
?
import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.w3c.dom.CharacterData; import org.w3c.dom.Comment; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.EntityReference; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import com.opensymphony.util.FileManager; import com.opensymphony.xwork.util.DomHelper; import com.opensymphony.xwork.util.TextParseUtil; import com.opensymphony.xwork.validator.Validator; import com.opensymphony.xwork.validator.ValidatorConfig; import com.opensymphony.xwork.validator.ValidatorFactory; /** * 表单配置文件加载 * * @author qianjun.liqj */ public class DefaultFormResolver implements FormResolver, Reloadable,InitializingBean { private static final Log LOG = LogFactory .getLog(DefaultFormResolver.class); private static final String MULTI_TEXTVALUE_SEPARATOR = " "; /** * 表单配置文件,格式为: form/form.xml,form/form1.xml */ private String configLocation = "form/form.xml"; /** * 表单配置,格式:groupName - group */ private Map<String, Group> groups = new HashMap<String, Group>(); public String getConfigLocation() { return configLocation; } public void setConfigLocation(String configLocation) { this.configLocation = configLocation; } /* * (non-Javadoc) * @see * com.xbuy.eshop.framework.validator.form.FormResolver#fetchGroup(java. * lang.String) */ public Group fetchGroup(String groupName) { if (groupName == null || groupName.trim().length() == 0) { return null; } return this.groups.get(groupName); } /* * (non-Javadoc) * @see * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() throws Exception { if (configLocation == null || configLocation.trim().length() == 0) { return; } this.reload(); } /** * Parse resource for a list of ValidatorConfig objects. * * @param is input stream to the resource * @param resourceName file name of the resource * @return List list of ValidatorConfig */ private void parseActionValidatorConfigs(InputStream is, final String resourceName) { InputSource in = new InputSource(is); in.setSystemId(resourceName); //设置DTD Map<String, String> dtdMappings = new HashMap<String, String>(); dtdMappings.put("-//alisoft software//DTD eshop webwork Validator 1.0//EN", "com/xbuy/eshop/framework/validator/eshop-validator-1.0.dtd"); Document doc = DomHelper.parse(in, dtdMappings); if (doc == null) { return; } //处理group NodeList groupNodes = doc.getElementsByTagName("group"); for (int j = 0; j < groupNodes.getLength(); j++) { Element groupElement = (Element) groupNodes.item(j); String groupName = groupElement.getAttribute("name"); if (groupName == null || groupName.trim().length() == 0) { continue; } Group group = new Group(); group.setName(groupName); //处理field NodeList fieldNodes = groupElement.getElementsByTagName("field"); for (int i = 0; i < fieldNodes.getLength(); i++) { List<ValidatorConfig> cfgs = new ArrayList<ValidatorConfig>(); Element fieldElement = (Element) fieldNodes.item(i); String fieldName = fieldElement.getAttribute("name"); Map<String, String> extraParams = new HashMap<String, String>(); extraParams.put("fieldName", fieldName); //处理 field-validator 列表 NodeList validatorNodes = fieldElement.getElementsByTagName("field-validator"); addValidatorConfigs(validatorNodes, extraParams, cfgs); //转化为 validator 列表 List<Validator> validators = new ArrayList<Validator>(cfgs.size()); for (Iterator<ValidatorConfig> iterator = cfgs.iterator(); iterator.hasNext();) { ValidatorConfig cfg = iterator.next(); Validator validator = ValidatorFactory.getValidator(cfg); validator.setValidatorType(cfg.getType()); validators.add(validator); } group.setValidators(fieldName, validators); } groups.put(groupName, group); } } /** * Extract trimmed text value from the given DOM element, ignoring XML * comments. Appends all CharacterData nodes and EntityReference nodes into * a single String value, excluding Comment nodes. This method is based on a * method originally found in DomUtils class of Springframework. * * @see org.w3c.dom.CharacterData * @see org.w3c.dom.EntityReference * @see org.w3c.dom.Comment */ private static String getTextValue(Element valueEle) { StringBuffer value = new StringBuffer(); NodeList nl = valueEle.getChildNodes(); boolean firstCDataFound = false; for (int i = 0; i < nl.getLength(); i++) { Node item = nl.item(i); if ((item instanceof CharacterData && !(item instanceof Comment)) || item instanceof EntityReference) { final String nodeValue = item.getNodeValue(); if (nodeValue != null) { if (firstCDataFound) { value.append(MULTI_TEXTVALUE_SEPARATOR); } else { firstCDataFound = true; } value.append(nodeValue.trim()); } } } return value.toString().trim(); } /** * 解析field节点下的所有field-validator子节点 * * @param validatorNodes field-validator节点列表 * @param extraParams 额外参数 * @param validatorCfgs ValidatorConfig结果列表 */ private void addValidatorConfigs(NodeList validatorNodes, Map<String, String> extraParams, List<ValidatorConfig> validatorCfgs) { for (int j = 0; j < validatorNodes.getLength(); j++) { Element validatorElement = (Element) validatorNodes.item(j); String validatorType = validatorElement.getAttribute("type"); //获取param Map<String, String> params = new HashMap<String, String>(extraParams); NodeList paramNodes = validatorElement.getElementsByTagName("param"); for (int k = 0; k < paramNodes.getLength(); k++) { Element paramElement = (Element) paramNodes.item(k); String paramName = paramElement.getAttribute("name"); params.put(paramName, getTextValue(paramElement)); } // ensure that the type is valid... ValidatorFactory.lookupRegisteredValidatorType(validatorType); ValidatorConfig vCfg = new ValidatorConfig(validatorType, params); vCfg.setLocation(DomHelper.getLocationObject(validatorElement)); vCfg.setShortCircuit(Boolean.valueOf(validatorElement.getAttribute("short-circuit")) .booleanValue()); NodeList messageNodes = validatorElement.getElementsByTagName("message"); Element messageElement = (Element) messageNodes.item(0); String key = messageElement.getAttribute("key"); if ((key != null) && (key.trim().length() > 0)) { vCfg.setMessageKey(key); } final Node defaultMessageNode = messageElement.getFirstChild(); String defaultMessage = (defaultMessageNode == null) ? "" : defaultMessageNode .getNodeValue(); vCfg.setDefaultMessage(defaultMessage); validatorCfgs.add(vCfg); } } @SuppressWarnings("unchecked") public void reload() { if (configLocation == null || configLocation.trim().length() == 0) { return; } this.groups.clear(); Set<String> configFiles = TextParseUtil.commaDelimitedStringToSet(configLocation); for (String fileName : configFiles) { InputStream is = null; try { is = FileManager.loadFile(fileName, this.getClass()); if (is != null) { parseActionValidatorConfigs(is, fileName); } } finally { if (is != null) { try { is.close(); } catch (IOException e) { LOG.error("Unable to close input stream for " + fileName, e); } } } } } }
?? 编写annotation类:Form 和 表单描述类 Group? ?
?
import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; /** * Marks a field or method param to read parameters from request. * * @author qianjun.liqj */ @Target( { ElementType.METHOD }) @Documented @Retention(value = RUNTIME) public @interface Form { /** * 设置表单校验对应的groupName. * * @return */ String group() default ""; }
?
/** * 表单验证组 * * @author qianjun.liqj */ public class Group { private static final Log LOG = LogFactory.getLog(Group.class); private String name; private List<String> fields = new ArrayList<String>(); private Map<String, List<Validator>> validators = new HashMap<String, List<Validator>>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public List<String> getFields() { return fields; } public void addField(String field) { this.fields.add(field); } public void setFields(List<String> fields) { this.fields = fields; } public Map<String, List<Validator>> getValidators() { return validators; } public void setValidators(String fieldName, List<Validator> validators) { this.validators.put(fieldName, validators); } public List<Validator> getValidators(String fieldName) { return this.validators.get(fieldName); } //TODO public List<Validator> getAllValidators() { List<Validator> validators = new ArrayList<Validator>(); for (Iterator<String> it = this.validators.keySet().iterator(); it.hasNext();) { validators.addAll(this.validators.get(it.next())); } return validators; } /** * Validates the given object using action . * * @param object the action to validate. * @throws ValidationException if an error happens when validating the * action. */ public void validate(Object object) throws ValidationException { ValidatorContext validatorContext = new DelegatingValidatorContext(object); validate(object, validatorContext); } /** * Validates an action give a validation context. * * @param object the action to validate. * @param validatorContext * @throws ValidationException if an error happens when validating the * action. */ public void validate(Object object, ValidatorContext validatorContext) throws ValidationException { List<Validator> validators = getAllValidators(); if (validators == null) return; Set<String> shortcircuitedFields = null; for (Iterator<Validator> iterator = validators.iterator(); iterator.hasNext();) { final Validator validator = iterator.next(); try { validator.setValidatorContext(validatorContext); if (LOG.isDebugEnabled()) { LOG.debug("Running validator: " + validator + " for object " + object); } FieldValidator fValidator = null; String fullFieldName = null; if (validator instanceof FieldValidator) { fValidator = (FieldValidator) validator; fullFieldName = fValidator.getValidatorContext().getFullFieldName( fValidator.getFieldName()); if ((shortcircuitedFields != null) && shortcircuitedFields.contains(fullFieldName)) { if (LOG.isDebugEnabled()) { LOG.debug("Short-circuited, skipping"); } continue; } } if (validator instanceof ShortCircuitableValidator && ((ShortCircuitableValidator) validator).isShortCircuit()) { // get number of existing errors List errs = null; if (fValidator != null) { if (validatorContext.hasFieldErrors()) { Collection fieldErrors = (Collection) validatorContext.getFieldErrors() .get(fullFieldName); if (fieldErrors != null) { errs = new ArrayList(fieldErrors); } } } else if (validatorContext.hasActionErrors()) { Collection actionErrors = validatorContext.getActionErrors(); if (actionErrors != null) { errs = new ArrayList(actionErrors); } } validator.validate(object); if (fValidator != null) { if (validatorContext.hasFieldErrors()) { Collection errCol = (Collection) validatorContext.getFieldErrors().get( fullFieldName); if ((errCol != null) && !errCol.equals(errs)) { if (LOG.isDebugEnabled()) { LOG.debug("Short-circuiting on field validation"); } if (shortcircuitedFields == null) { shortcircuitedFields = new TreeSet(); } shortcircuitedFields.add(fullFieldName); } } } else if (validatorContext.hasActionErrors()) { Collection errCol = validatorContext.getActionErrors(); if ((errCol != null) && !errCol.equals(errs)) { if (LOG.isDebugEnabled()) { LOG.debug("Short-circuiting"); } break; } } continue; } else {// validator.validate(object); } } finally { validator.setValidatorContext(null); } } } }
?? ok,到此所有的代码编写完毕。
?
七、使用方法
???? 在Action.doXxxx()方法增加标注,编写配置文件即可。如下:
?
@Form(group="login") @Override public final String execute(){}
?? 配置文件见上述例子
?
八、进一步的想法
????? 鉴于spring+webwork/struts2.0 +velocity的结构,对于表单的前端校验需要自己来实现,而且不能和webwork的表单校验框架联系起来,这是一个遗憾。
????? 我觉得可以通过JS来实现一个表单的前端校验框架,详细的校验方法由webwork的表单校验器来生成,在渲染页面的时候把对应的JS Function 渲染到页面中。当submit表单的时候,该Js框架拦截Form.submit()事件执行表单校验,该部分可参考valang的实现机制。
?????? 另外,在表单的csrf攻击上也可通过验证器统一处理。
?
?
?
?
?
?
?
?