validation - niubods/playframework-notes GitHub Wiki

用Play验证HTTP数据

验证可以保证数据有正确的值或者符合特殊要求。你可以使用验证来确保你的模型在保存到数据库之前是正确的,或者把它们直接用在HTTP参数上来验证简单的表单。

如何在Play中使用表单验证

每一个请求都有它自己的Validation对象来保持错误信息。有三种方法可以定义验证。

  1. 在一个控制器方法中,直接调用控制器validation字段中的方法。你也可以通过使用play.data.validation.Validation类的静态方法来访问API的一个子集。
  2. 在控制器方法的参数声明上添加验证注解。
  3. 在控制器方法的POJO参数上添加 @Valid 注解,然后在POJO属性上添加验证注解。

validation对象保持着一个play.data.validation.Error对象的集合。每一个error有两个属性:

  • key。这个属性帮你确定是哪一个数据元素导致了这个错误。key值可以是任意的,但当Play产生错误的时候,它会遵照Java变量名的默认约定。
  • message。这个属性包含错误的字面量说明信息。这个消息可以是纯文本的消息,或者是一个message bondle(特别是对于国际化支持来说)中的key。

使用第一种方法,我们来看看如何验证一个简单的HTTP参数:

public static void hello(String name) {
     validation.required(name);
     …
}

这段代码检查是否正确设置了name变量。如果没有,相应的错误就会加入到当前的errors集合中。

你可以为你需要的每一个验证重复这一操作:

public static void hello(String name, Integer age) {
     validation.required(name);
     validation.required(age);
     validation.min(age, 0);
     …
}

验证的错误消息

在验证的最后你可以检查一下有没有造成错误并且把它们显示出来:

public static void hello(String name, Integer age) {
     validation.required(name);
     validation.required(age);
     validation.min(age, 0);
     
     if(validation.hasErrors()) {
         for(Error error : validation.errors()) {
             System.out.println(error.message());
         }
     }
}

假设name和age都是null,这样会显示:

Required
Required

这是因为在PLAY_HOME/resources/messages中定义的默认消息是:

validation.required=Required

有三种方法来自定义验证消息。

  1. 通过重新定义你应用中的message文件来覆盖默认消息。
  1. 提供一个自定义消息作为附加的验证参数。
  1. 提供一个对应本地化消息的key作为附加的验证参数。

本地化验证消息

覆盖这些消息最简单的方法是使用在你应用的conf/message写一个有相同key的消息。例如:

validation.required = Please enter a value

你也可以使用其他语言提供本地化消息,详见国际化

验证消息参数

你可以在消息中为error的key设置一个占位符:

validation.required=%s is required

这样输出就会变成:

name is required
age is required

限制:当超过一个使用validation.required(age)语法的必填字段验证失败时,Play不能判断正确的参数名。在这种情况下,你必须直接指定字段名称,例如validation.required("age", age)

这个error的key默认为参数名,而且它本身用来查找一条消息。例如,上面hello动作方法中的name参数可以本地化成:

name = Customer name

这样会在输出的结果是:

Custormer name is required
age is required

你可以使用error.message(String key)方法覆盖error的key。例如:

Error error = validation.required(name).error;
if(error != null) {
	System.out.println(error.message("Customer name"));
}

很多内置验证额外定义了一个和验证参数对应的消息参数。例如,’match’验证定义了第二个String参数来对应指定的正则表达式,和上面%s占位符的不同之处在于它指定了参数索引’2’:

validation.match=Must match %2$s

相似地,’range’验证额外定义了两个数字参数,索引为2和3:

validation.range=Not in the range %2$d throw %3$d

$PLAY_HOME/resourecs/message文件里看看,其他还有那些验证有参数。

自定义本地化验证消息

$PLAY_HOME/resourecs/message中的验证消息为每一个paly的内置验证使用默认的消息key。你可以指定一个不同的消息key。例如:

validation.required.em = You must enter the %s!

给消息使用这个新的消息key,对于在动作方法中的手动验证:

validation.required(manualKey).message("validation.required.em");

另一种选择是,在注解的message参数中使用这个key:

public static void hello(@Required(message="validation.required.em") String name) {
	…
}

你可以在JavaBean属性的验证注解中使用同样的技术:

public static void hello(@Valid Person person) { 
   …
} 
 
public class Person extends Model { 
   @Required(message = "validation.required.emphasis")
   public String name; 
   … 
} 

自定义字面量(不进行本地化)验证消息

如果对于一个key没有定义相应的消息,Play的消息查找只是返回消息key,这意味着如果你愿意,你可以只使用一个消息字面量而不是消息key。使用和上面相同的例子,对于手动验证:

validation.required(manualKey).message("Give us a name!"); 

对于动作方法的参数注解:

public static void save(@Required(message = "Give us a name!") String name) { 
   …
} 

对于JavaBean属性注解:

public static void save(@Valid Person person) { 
   …
}
 
public class Person extends Model {
   @Required(message = "Give us a name!")
   public String name; 
   … 
} 

验证错误消息在模板中的显示

在多数情况下你想要在视图模板中显示错误消息。你可以在模板中使用error对象来访问它们。有一些标签可以帮助你显示这些错误。

我们先来看一个例子:

public static void hello(String name, Integer age) {
   validation.required(name);
   validation.required(age);
   validation.min(age, 0);
   render(name, age);
}

现在再来看模板:

#{ifErrors}
 
   <h1>Oops…</h1>
 
   #{errors}
       <li>${error}</li>
   #{/errors}
 
#{/ifErrors}
#{else}
 
   Hello ${name}, you are ${age}.
 
#{/else}

但是在真正的应用中你想要重新显示原来的表单。所以你要准备两个动作:一个用来显示表单而另一个用来处理POST请求。

当然验证会发证在第二个动作中而且如果产生了错误你必须重定向到第一个动作。在这种情况下你需要一个特殊的技巧在重定向时保持你的错误信息。使用validation.keep()方法。这样可以把错误信息集合保持到下一个动作。

来看一个真实的例子:

public class Application extends Controller {
 
   public static void index() {
      render();
   }
 
   public static void hello(String name, Integer age) {
      validation.required(name);
      validation.required(age);
      validation.min(age, 0);
      if(validation.hasErrors()) {
          params.flash(); // add http parameters to the flash scope
          validation.keep(); // keep the errors for the next request
          index();
      }
      render(name, age);
   }
 
}

还有view/Application/index.html模板:

#{ifErrors}
   <h1>Oops…</h1>
 
   #{errors}
       <li>${error}</li>
   #{/errors}
#{/ifErrors}
 
#{form @Application.hello()}
   <div>
      Name: <input type="text" name="name" value="${flash.name}" />
   </div>
   <div>
      Age: <input type="text" name="age" value="${flash.age}" /> 
   </div>
   <div>
      <input type="submit" value="Say hello" /> 
   </div>
#{/form}

你可以在每一个产生错误的字段后面显示各自的错误信息以此创造更好的用户体验:

#{ifErrors}
   <h1>Oops…</h1>
#{/ifErrors}
 
#{form @Application.hello()}
   <div>
      Name: <input type="text" name="name" value="${flash.name}" />
      <span class="error">#{error 'name' /}</span>
   </div>
   <div>
      Age: <input type="text" name="age" value="${flash.age}" /> 
      <span class="error">#{error 'age' /}</span>
   </div>
   <div>
      <input type="submit" value="Say hello" /> 
   </div>
#{/form}

验证注解



public static void hello(@Required String name, @Required @Min(0) Integer age) {
   if(validation.hasErrors()) {
       params.flash(); // add http parameters to the flash scope
       validation.keep(); // keep the errors for the next request
       index();
   }
   render(name, age);
}

验证复杂对象

你也可以使用验证注解来方便的给你的模型对象属性添加约束,然后在控制器中指定所有属性必须要验证。我们来用User类重写一下前面的那个例子。

首先是User类,在属性上使用验证注解:

package models;
 
public class User {
    
    @Required
    public String name;
 
    @Required
    @Min(0)
    public Integer age;
}

然后修改hello动作,使用Valid注解来指定所有User对象的属性必须要验证:

public static void hello(@Valid User user) {
   if(validation.hasErrors()) {
       params.flash(); // add http parameters to the flash scope
       validation.keep(); // keep the errors for the next request
       index();
   }
   render(name, age);
}

最后是修改表单:

#{ifErrors}
   <h1>Oops…</h1>
#{/ifErrors}
 
#{form @Application.hello()}
   <div>
      Name: <input type="text" name="user.name" value="${flash['user.name']}" />
      <span class="error">#{error 'user.name' /}</span>
   </div>
   <div>
      Age: <input type="text" name="user.age" value="${flash['user.age']}" /> 
      <span class="error">#{error 'user.age' /}</span>
   </div>
   <div>
      <input type="submit" value="Say hello" /> 
   </div>
#{/form}

内置验证

使用@CheckWith进行自定义验证

play.data.validation包中没有找到需要的验证器?那就自己写一个吧。你可以使用通用的CheckWith注解来绑定你自己的Check实现。

举个例子:

public class User {
    
    @Required
    @CheckWith(MyPasswordCheck.class)
    public String password;
    
    static class MyPasswordCheck extends Check {
        
        public boolean isSatisfied(Object user, Object password) {
            return notMatchPreviousPasswords(password);
        }
    }
}

默认的验证错误消息key是validation.invalid。要使用别的key,用消息key和消息参数调用Check.setMessage

static class MyPasswordCheck extends Check {
 
    public boolean isSatisfied(Object user, Object password) {
        final Date lastUsed = dateLastUsed(password);
        setMessage("validation.used", JavaExtensions.format(lastUsed));
        return lastUsed == null;
    }
}

消息查找总是会以字段名作为第一个参数,而你的消息参数就作为随后的参数。所以,对于上面的例子,你可以像这样定义消息:

validation.used = &{%1$s} already used on date %2$s
user.password = Password

这里&{%1$s}使用索引为1的消息参数(字段名)作为消息key来查找其他消息,而%2$s为第二消息参数(格式化过的日期)。

消息语法——%s%s2$s&{…}——在怎样获取本地化消息这一节详细阐述。

自定义注解

你还可以编写你自己的注解验证,这更复杂一些但是可以使你的模型代码更整洁并且让你可以引用验证器参数。

举个例子,假设我们想要一个@URL验证的宽松版,那样我们可以允许URL使用任意协议比如一个file://URL,还有一个参数让我们可以精确指定那些协议是允许的。

首先,我们写一个自定义验证注解,有一个参数来覆盖覆盖默认消息:

import net.sf.oval.configuration.annotation.Constraint;
import java.lang.annotation.*;
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(checkWith = URICheck.class)
public @interface URI {
    String message() default URICheck.message;
}

这个注解引用了net.sf.oval.configuration.annotation.AbstractAnnotationCheck这个实现。

public class URICheck extends AbstractAnnotationCheck<URI> {
 
    /** Error message key. */
    public final static String message = "validation.uri";
 
    /** URI schemes allowed by validation. */
    private List<String> schemes;
 
    @Override
    public void configure(URI uri) {
        setMessage(uri.message());
        this.schemes = Arrays.asList(uri.schemes());
    }
 
    /**
     * Add the URI schemes to the message variables so they can be included
     * in the error message.
     */
    @Override
    public Map<String, String> createMessageVariables() {
        final Map<String, String> variables = new TreeMap<String, String>();
        variables.put("2", JavaExtensions.join(schemes, ", "));
        return variables;
    }
 
    @Override
    public boolean isSatisfied(Object validatedObject, Object value,
        OValContext context, Validator validator) throws OValException {
 
        requireMessageVariablesRecreation();
        try {
            final java.net.URI uri = new java.net.URI(value.toString());
            final boolean schemeValid = schemes.contains(uri.getScheme());
            return schemes.size() == 0 || schemeValid;
        } catch (URISyntaxException e) {
            return false;
        }
    }
}

isSatisfied方法调用requireMessageVariableRecreation()来通知OVal在渲染消息之前去调用createMessageVariales()。这返回一个按传入消息格式化器的变量顺序排序的map。map的key没用到;这个例子中的"2"表明消息参数的索引。前面已经说过,第一个参数是字段名。

要使用这个的话在模型的属性上使用这个注解。

public class User {
    
    @URI(message = "validation.uri.schemes", schemes = {"http", "https"})
    public String profile;
}

我们可以这样定义消息:

validation.uri = Not a valid URI
validation.uri.schemes = &{%1$s} is not a valid URI - allowed schemes are %2$s
⚠️ **GitHub.com Fallback** ⚠️