使用 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"));
我有一个动态模型,由 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"));