读书人

对于webwork的表单校验的改善

发布时间: 2012-10-28 09:54:44 作者: rapoo

对于webwork的表单校验的改进

?

一、来由

??? ? 最近一段时间使用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 groupname CDATA #REQUIRED><!ELEMENT field (field-validator+)><!ATTLIST fieldname CDATA #REQUIRED><!ELEMENT field-validator (param*, message)><!ATTLIST field-validatortype CDATA #REQUIREDshort-circuit (true | false) "false"><!ELEMENT validator (param*, message)><!ATTLIST validatortype CDATA #REQUIREDshort-circuit (true | false) "false"><!ELEMENT param (#PCDATA)><!ATTLIST paramname CDATA #REQUIRED><!ELEMENT message (#PCDATA)><!ATTLIST messagekey 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攻击上也可通过验证器统一处理。

?

?

?

?

?

?

?

?

读书人网 >Web前端

热点推荐