Play 2.6 表单

Java表单

英文原文
https://playframework.com/documentation/2.6.x/JavaForms

在使用表单之前,可以先看一下Play enhancer,Play enhancer为Java类中的域生成访问方法,这样你就不需要手动添加这些代码,这也许会为你提供一些便利。下面展示的例子都是手工生成访问方法的。

启用/禁用表单组件

默认情况下,如果启动PlayJava SBT插件,Play会将Java表单组件包含进来。那么你只需要在工程中enablePlugins(PlayJava)

表单组件也可以通过PlayImport导入,可以在build.sbt中添加libraryDependencies += javaForms,(个人理解两种方式使用一种即可,可以获取Play提供的事例来查看具体配置)

Note: 如果你不需要表单的功能,可以通过使用PlayMinimalJava插件来取代PlayJava。这也可以帮帮助你去除过多的依赖,包括Spring组件以及Hibernate validator

表单定义

play.data包中包含了一下工具来处理HTTP表单数据的提交及验证。最简单的方法是定义一个play.data.Form来封装一个已经存在的类

public class User {

    protected String email;
    protected String password;

    public void setEmail(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPassword() {
        return password;
    }

}

为了封装你的类需要将一个play.data.FormFactory注入到Controller中,然后你可以创建表单

Form<User> userForm = formFactory.form(User.class);

数据绑定是通过Spring data binder完成的。

这个表单对象可以从一个HashMap

Map<String,String> anyData = new HashMap<>();
anyData.put("email", "bob@gmail.com");
anyData.put("password", "secret");

User user = userForm.bind(anyData).get();

如果当前有一个request可以,可以直接从reqeust中获取内容

User user = userForm.bindFromRequest().get();

定义限制条件

可以使用JSR-303标准中的注解来添加额外的限制

public class User {

    @Required
    protected String email;
    protected String password;

    public void setEmail(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPassword() {
        return password;
    }

}
play.data.validation.Constraints类中包含了大量的内建注解

后面的高级验证会介绍更多内容。

处理绑定失败

在添加了限制条件之后,需要去处理数据绑定失败

if (userForm.hasErrors()) {
    return badRequest(views.html.form.render(userForm));
} else {
    User user = userForm.get();
    return ok("Got user " + user);
}

通常会将表单直接发送给模板。一个全局的错误可以这么处理:

@if(form.hasGlobalErrors) {
    <p class="error">
        @for(error <- form.globalErrors) {
            <p>@error.format(messages())</p>
        }
    </p>
}

特定域中的错误可以用error.format处理:

@for(error <- form("email").errors) {
    <p>@error.format(messages())</p>
}

error.format使用一个message()作为参数,这是定义在Javal18n中一个play.18n.Messages的实例

使用默认值填充表单

userForm = userForm.fill(new User("bob@gmail.com", "secret"));

表单对象是不可变的,调用bind()或者fill()方法会返回一个新的对象。

处理与model不相关联的表单

可以使用DynamicForm来从html表单中获取数据

public Result hello() {
    DynamicForm requestData = formFactory.form().bindFromRequest();
    String firstname = requestData.get("firstname");
    String lastname = requestData.get("lastname");
    return ok("Hello " + firstname + " " + lastname);
}

注册自定义的DataBinder

如果你想自己定义表单中的String域与对象之间的映射关系,你需要为这个类型注册一个新的Formatters.

你可以为Fromatters注册一个provider,Formatters会进行合适的初始化

对于一个LocalTime对象,可以这样处理

import java.text.ParseException;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;

import java.time.LocalTime;

import play.data.format.Formatters;
import play.data.format.Formatters.SimpleFormatter;
import play.i18n.MessagesApi;


@Singleton
public class FormattersProvider implements Provider<Formatters> {

    private final MessagesApi messagesApi;

    @Inject
    public FormattersProvider(MessagesApi messagesApi) {
        this.messagesApi = messagesApi;
    }

    @Override
    public Formatters get() {
        Formatters formatters = new Formatters(messagesApi);

        formatters.register(LocalTime.class, new SimpleFormatter<LocalTime>() {

            private Pattern timePattern = Pattern.compile(
                    "([012]?\\d)(?:[\\s:\\._\\-]+([0-5]\\d))?"
            );

            @Override
            public LocalTime parse(String input, Locale l) throws ParseException {
                Matcher m = timePattern.matcher(input);
                if (!m.find()) throw new ParseException("No valid Input", 0);
                int hour = Integer.valueOf(m.group(1));
                int min = m.group(2) == null ? 0 : Integer.valueOf(m.group(2));
                return LocalTime.of(hour, min);
            }

            @Override
            public String print(LocalTime localTime, Locale l) {
                return localTime.format(DateTimeFormatter.ofPattern("HH:mm"));
            }

        });

        return formatters;
    }
}

在定义了provider之后在进行绑定

import com.google.inject.AbstractModule;

import play.data.format.Formatters;

public class FormattersModule extends AbstractModule {

    @Override
    protected void configure() {

        bind(Formatters.class).toProvider(FormattersProvider.class);

    }
}

最后在application.conf中停用Play默认的FormattersModule然后启用你自己定义的

play.modules.enabled += "com.example.FormattersModule"
play.modules.disabled += "play.data.format.FormattersModule"

绑定失败时出产生一个错误信息的数组,数组中的第一个元素在messages文件中定义,数组包含以下内容

["error.invalid.<fieldName>", "error.invalid.<type>", "error.invalid"]

错误信息是由 Spring DefaultMessageCodesResolver产生的,根“typeMismathc”被替换为”error.invalid”

高级验证

Play内建的验证模块是通过Hibernate Validator实现的。也就说说我们可以使用在JSR-303和JSR-349中所定义的特性。Hibernate Validator的文章可以参考这里

域交叉验证

可以使用 class-level constraints来验证整个对象的状态。Play提供了一些常用的实现来满足最基本的需求。

来看一下具体是如何工作的:为了定义一个ad-hoc validation,你所需要做的金金在在你的表单类上添加一个class-level constraints(@Validate)然后实现相应的接口(下面的例子中是Validatable

import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;

@Validate
public class User implements Validatable<String> {

    @Constraints.Required
    protected String email;
    protected String password;

    @Override
    public String validate() {
        if (authenticate(email, password) == null) {
            // You could also return a key defined in conf/messages
            return "Invalid email or password";
        }
        return null;
    }

    // getters and setters

}

例子中的返回信息会变成一个全局的错误。错误信息在play.data.validation.ValidationError中定义,

需要注意到validate方法和@Constraints.Required限制是同事调用的——所以无论注解有没有成功,validate方法都会执行。后续内容会介绍如何定义调用的顺序。

从例子中也可以看出Validatable接收的参数类型决定了validate()方法的返回类型。

你可以选择String、ValidationError或者List作为返回值,这取决于逆向返回一个全局错误,还是一个错误(也可以是全局的),还是多个错误(也许是全局的)。其他类型的返回值都会被Play忽略。

如果验证通过,就必须返回Null,或者空的List,其他类型的返回值都会被视为验证失败。

当你对一些特殊域有额外的验证时,返回ValidationError会比较有用。

import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;

@Validate
public static class LoginForm implements Validatable<ValidationError> {

    // fields, getters, setters, etc.

    @Override
    public ValidationError validate() {
        if (authenticate(email, password) == null) {
            // Error will be displayed for the email field:
            return new ValidationError("email", "Invalid credentials");
        }
        return null;
    }
}

也可以通过返回List来添加多个验证错误,用于添加单个域的错误,全局错误以及它们的组合。

import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import java.util.List;

@Validate
public static class SignUpForm implements Validatable<List<ValidationError>> {

    // fields, getters, setters, etc.

    @Override
    public List<ValidationError> validate() {
        final List<ValidationError> errors = new ArrayList<>();
        if (authenticate(email, password) == null) {
            // Add an error which will be displayed for the email field:
            errors.add(new ValidationError("email", "Access denied"));
            // Also add a global error:
            errors.add(new ValidationError("", "Form could not be submitted"));
        }
        return errors;
    }
}

当ValidationError中的key值为空字符串时,就会成为一个全局错误。

另外一点,出来自己来写错误信息,也可以使用在conf/meaages中定义的key值,然后将参数传递过去。在模板中展示验证错误信息时,Play会自动解析key值和参数值。

// Global error without internationalization:
new ValidationError("", "Errors occured. Please check your input!");
// Global error; "validationFailed" should be defined in `conf/messages` - taking two arguments:
new ValidationError("", "validationFailed", Arrays.asList(arg1, arg2));
// Error for the email field; "emailUsedAlready" should be defined in `conf/messages` - taking the email as argument:
new ValidationError("email", "emailUsedAlready", Arrays.asList(email));
通过groups来部分验证

当用户提交表单时,你可能只想对限制条件中的一部分进行验证。比如在一个UI向导中,每一步只需要验证一个子集。

或者考虑一下注册域登录过程。在这两个过程中你需要用户输入邮箱和密码,所以这两个过程需要验证同样的表单。考虑一个例子,用户在登录后修改配置页面的信息,这会需要第三个form。

使用三个不同的表单并不是一个好主意,因为在一些域中你会使用相同的验证。如果一个域name你设置了最大长度为255,然后你想要改成100,你需要分别修改三个表单。

Play中可以使用group constraints来简化这一过程。

import play.data.validation.Constraints;
import play.data.validation.Constraints.Validate;
import play.data.validation.Constraints.Validatable;
import play.data.validation.ValidationError;
import javax.validation.groups.Default;

@Validate(groups = {SignUpCheck.class})
public class PartialUserForm implements Validatable<ValidationError> {

    @Constraints.Required(groups = {Default.class, SignUpCheck.class, LoginCheck.class})
    @Constraints.Email(groups = {Default.class, SignUpCheck.class})
    private String email;

    @Constraints.Required
    private String firstName;

    @Constraints.Required
    private String lastName;

    @Constraints.Required(groups = {SignUpCheck.class, LoginCheck.class})
    private String password;

    @Constraints.Required(groups = {SignUpCheck.class})
    private String repeatPassword;

    @Override
    public ValidationError validate() {
        if (!checkPasswords(password, repeatPassword)) {
            return new ValidationError("repeatPassword", "Passwords do not match");
        }
        return null;
    }

    // getters and setters

}

SignUpChkeckLoginChkeckgroup被定义为两个接口

public interface SignUpCheck { }
public interface LoginCheck { }

对于注册过程,我们可以将SignUpcheck给form()方法

Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, SignUpCheck.class).bindFromRequest();

这个例子中邮箱是必须的而且要是正确的格式,密码和密码确认要保持一致(这个通过validate方法 )。在注册过程名字相关域我们可以不用关系。

对于登录过程,我们可以使用LoginCheckgroup

Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, LoginCheck.class).bindFromRequest();

现在我们仅要求邮箱和密码,邮箱名是否合法也不用关心。

现在假设有一个页面用户可以修改除密码意外的新

Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, Default.class).bindFromRequest();

这实际上与以下代码相同:

Form<PartialUserForm> form = formFactory().form(PartialUserForm.class).bindFromRequest();

这个例子中除了密码以外的域都会进行验证,一个域如果没有指明就会被归到Default group中。由于密码域中显示定义了group,所以在这里不会被验证。

可以通过from()方法来添加多个组

还有一种高级用法是将一组限制添加到另一个组中,具体可以参考以下文档

定义group的验证顺序

group之间可以以一定的顺序来验证,如果中间有一个验证失败,后面的group就不会验证(目前组内部限制条件的验证顺序还无法控制)

import javax.validation.GroupSequence;
import javax.validation.groups.Default;

@GroupSequence({ Default.class, SignUpCheck.class, LoginCheck.class })
public interface OrderedChecks { }
Form<PartialUserForm> form = formFactory().form(PartialUserForm.class, OrderedChecks.class).bindFromRequest();

在使用group sequence时会首先验证Default组,在Default验证通过之后才会验证SignUp。

如果你的validate方法需要执行一个数据库查询或者其他阻塞方法,使用group sequence是一个很好的方法:如果在一些低级别验证失败时(比如email不合法),方法的执行就没有任何效果。在这个例子中你希望所有的注解都验证通过之后才去执行validate方法。比如一个用户在注册时,需要先验证这个email是否合法再去数据库中查询邮箱是否被占用。

通过DI来自定义class-level constraints

有时你会需要更加复杂的验证过程。比如验证一个有限是否已经存在

由于限制支持运行时的DI,所以我们可以创建自己的限制(注入一个DataBase对象用于后续的验证)。当然MessageAPI和JPAApi也是可以的

Note: 只有在一些each cross concern才需要用到这个技术。比如这一节我们创建的限制是可以重用的,并且在所有需要访问数据库的验证中都会使用。Play不支持在一般的class-level constraints注入的原因是Play不知道你会在项目中启用哪个模块。

首先创建一个拥有validate方法的接口,方法的参数为一个Database对象( database docs

import play.db.Database;

public interface ValidatableWithDB<T> {
    public T validate(final Database db);
}

同时我们也需要一个 class-level 注解

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidateWithDBValidator.class)
public @interface ValidateWithDB {
    String message() default "error.invalid";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

接下来是限制的实现类

import java.util.List;

import javax.inject.Inject;
import javax.validation.ConstraintValidatorContext;

import play.data.validation.Constraints.PlayConstraintValidator;

import play.db.Database;

public class ValidateWithDBValidator implements PlayConstraintValidator<ValidateWithDB, ValidatableWithDB<?>> {

    private final Database db;

    @Inject
    public ValidateWithDBValidator(final Database db) {
        this.db = db;
    }

    @Override
    public void initialize(final ValidateWithDB constraintAnnotation) {
    }

    @Override
    public boolean isValid(final ValidatableWithDB<?> value, final ConstraintValidatorContext constraintValidatorContext) {
        return reportValidationStatus(value.validate(this.db), constraintValidatorContext);
    }
}

Darabase对象在构造器中被注入。如果使用运行时注入,Guice会自动帮我们注入,如果是编译期的注入,则需要手动添加:

import play.ApplicationLoader;
import play.BuiltInComponentsFromContext;
import play.data.FormFactoryComponents;
import play.data.validation.MappedConstraintValidatorFactory;
import play.db.DBComponents;
import play.db.HikariCPComponents;
import play.filters.components.NoHttpFiltersComponents;
import play.routing.Router;

public class ValidateWithDBComponents extends BuiltInComponentsFromContext
        implements FormFactoryComponents, DBComponents, HikariCPComponents, NoHttpFiltersComponents {

    public ValidateWithDBComponents(ApplicationLoader.Context context) {
        super(context);
    }

    @Override
    public Router router() {
        return Router.empty();
    }

    @Override
    public MappedConstraintValidatorFactory constraintValidatorFactory() {
        return new MappedConstraintValidatorFactory()
                .addConstraintValidator(
                        ValidateWithDBValidator.class,
                        new ValidateWithDBValidator(database("default"))
                );
    }
}
Note: 不需要创建自己的database实例,这在接口中已经定义了

当实现自己的class-level constraints时,传递给reportValidationStatus方法的参数为一个ValidationError,一个List,或者一个字符串。其他类型都会被忽略。现在可以使用我们的class-level constraint

import play.data.validation.Constraints;
import play.data.validation.ValidationError;
import play.db.Database;

@ValidateWithDB
public class DBAccessForm implements ValidatableWithDB<ValidationError> {

    @Constraints.Required
    @Constraints.Email
    private String email;

    @Constraints.Required
    private String firstName;

    @Constraints.Required
    private String lastName;

    @Constraints.Required
    private String password;

    @Constraints.Required
    private String repeatPassword;

    @Override
    public ValidationError validate(final Database db) {
        // Access the database to check if the email already exists
        if (User.byEmail(email, db) != null) {
            return new ValidationError("email", "This e-mail is already registered.");
        }
        return null;
    }

    // getters and setters

}
Tip: 你可以通过实现更多接口来添加更多的class-level constraint annotations。通过验证groups 你可以只是调用你期待的验证方法(或者在一个过程中处理多个验证)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值