一、来由
最近一段时间使用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攻击上也可通过验证器统一处理。