使用 Spring MVC 绑定表单对象 CGLIB 的动态表单

Dynamic Form with Spring MVC bind form object CGLIB

我有一个动态模型,由 X 数字的广播、文本、select、select-multi 组成。这基本上就像后端的EAV数据库。

我需要用总共 N 个字段来呈现此动态表单,然后验证提交的动态模型对象,然后使用字段定义的正则表达式对其进行验证。我希望通过 JSR 303 注释执行此验证。

所以,我想使用 Spring MVC 开发的典型方式,通过 ModelAttribute @Valid 特性等来绑定表单对象。唯一的区别是模型对象 unknown/undefined 直到运行时。

我倾向于使用 CGLIB 或类似的东西在 运行 时生成 class 并用特殊的 taglib 呈现它,然后使用反射以某种方式使用特殊验证来验证它。

这样的事情是完全不可能的吗?同样,我想做常规的 Spring MVC 控制器和模型,但使用动态表单对象。

控制器class(RegisterController.java)编写代码如下:

File: src/main/java/net/codejava/spring/controller/RegisterController.java

package net.codejava.spring.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import net.codejava.spring.model.User;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value = "/register")
public class RegisterController {

    @RequestMapping(method = RequestMethod.GET)
    public String viewRegistration(Map<String, Object> model) {
        User userForm = new User();    
        model.put("userForm", userForm);

        List<String> professionList = new ArrayList<>();
        professionList.add("Developer");
        professionList.add("Designer");
        professionList.add("IT Manager");
        model.put("professionList", professionList);

        return "Registration";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String processRegistration(@ModelAttribute("userForm") User user,
            Map<String, Object> model) {

        // implement your own registration logic here...

        // for testing purpose:
        System.out.println("username: " + user.getUsername());
        System.out.println("password: " + user.getPassword());
        System.out.println("email: " + user.getEmail());
        System.out.println("birth date: " + user.getBirthDate());
        System.out.println("profession: " + user.getProfession());

        return "RegistrationSuccess";
    }
}

我们可以看到这个controller是用来处理请求的URL /register:

@RequestMapping(value = "/register")

我们实现了两个方法 viewRegistration() 和 processRegistration() 来分别处理 GET 和 POST 请求。在 Spring 中编写处理程序方法非常灵活,我们可以自由选择自己的方法名称和必要的参数。下面我们详细看看上面控制器class的每个方法: viewRegistration():在这个方法中,我们创建了一个模型对象,并将其放入模型映射中,键为“userForm”:

User userForm = new User();
model.put("userForm", userForm);

这会在指定对象与此方法返回的视图中的表单(即注册表单)之间创建绑定。请注意,键“userForm”必须与标签的 commandName 属性的值相匹配。

另一个有趣的地方是,我们创建了一个字符串列表,并将其放入带有键“professionList”的模型映射中:

List<String> professionList = new ArrayList<>();
professionList.add("Developer");
professionList.add("Designer");
professionList.add("IT Manager");
model.put("professionList", professionList);

此集合将由 Registration.jsp 页面中的标记使用,以动态呈现职业下拉列表。 最后这个方法 returns 一个视图名称(“Registration”)将被映射到上面的注册表单页面。 processRegistration():此方法处理表单提交(通过 POST 请求)。这里重要的参数是:

@ModelAttribute("userForm") User user

这将使方法主体可以使用模型映射中存储在键“userForm”下的模型对象。同样,键“userForm”必须与标签的 commandName 属性值相匹配。 当提交表单时,Spring自动将表单的字段值绑定到模型中的后台对象,这样我们就可以通过这个后台对象访问用户输入的表单值,如下所示:

System.out.println("username: " + user.getUsername());

出于演示目的,此方法仅打印出 User 对象的详细信息,最后 returns 成功页面的视图名称(“RegistrationSuccess”)。

这是可能的,但不能使用 cglib。 Cglib 是一个代理库,只允许您覆盖现有的方法,但不能定义新的方法,以便将动态模型传递给 Spring 或任何其他 bean 验证库。

当然可以使用 Javassist 或 Byte Buddy(披露:我是作者)生成这样的 classes,它允许您定义这样的元数据。例如,使用 Byte Buddy,您可以定义动态 class:

new ByteBuddy()
  .subclass(Object.class)
  .defineField("foo", String.class, Visibility.PUBLIC)
  .annotateField(AnnotationDescription.Builder.ofType(NotNull.class).build())
  .make();

使用此代码,您可以加载 class,为 "foo" 字段分配一个值并将此 bean 对象提交给 bean 验证库。

同时,我觉得这种方法设计过度了。如果您需要为无法重新实现的特定 JSR 303 验证器提供兼容性,我只会采用此解决方案。否则,您可能更愿意直接验证数据。

如果您的框架还需要 getter 和 setter,您可以通过以下方式添加它们:

builder = builder
  .defineMethod("getFoo", String.class, Visibility.PUBLIC)
    .intercept(FieldAccessor.ofField("foo"));
  .defineMethod("setFoo", void.class, Visibility.PUBLIC)
    .withParameters(String.class)
    .intercept(FieldAccessor.ofField("foo"));