春季验证,如何让属性编辑器生成特定的错误消息

2022-09-03 15:02:11

我正在使用Spring进行表单输入和验证。窗体控制器的命令包含正在编辑的模型。模型的某些属性是自定义类型。例如,“人员”的社会安全号码是自定义 SSN 类型。

public class Person {
    public String getName() {...}
    public void setName(String name) {...}
    public SSN getSocialSecurtyNumber() {...}
    public void setSocialSecurtyNumber(SSN ssn) {...}
}

并将“人”包装在 Spring 表单编辑命令中:

public class EditPersonCommand {
    public Person getPerson() {...}
    public void setPerson(Person person) {...}
}

由于Spring不知道如何将文本转换为SSN,因此我使用表单控制器的活页夹注册了一个客户编辑器:

public class EditPersonController extends SimpleFormController {
    protected void initBinder(HttpServletRequest req, ServletRequestDataBinder binder) {
        super.initBinder(req, binder);
        binder.registerCustomEditor(SSN.class, "person.ssn", new SsnEditor());
    }
}

SsnEditor 只是一个可以将文本转换为 SSN 对象的自定义:java.beans.PropertyEditor

public class SsnEditor extends PropertyEditorSupport {
    public String getAsText() {...} // converts SSN to text
    public void setAsText(String str) {
        // converts text to SSN
        // throws IllegalArgumentException for invalid text
    }
}

如果遇到无效且无法转换为 SSN 的文本,则抛出(根据 的规范)。我遇到的问题是,文本到对象的转换(通过)发生在我的Spring验证器被调用之前。当抛出时,Spring只显示 中定义的一般错误消息。我想要的是一个特定的错误消息,该消息取决于输入的SSN无效的确切原因。 将确定原因。我尝试在 的文本中嵌入错误原因文本,但Spring只是将其视为一般错误。setAsTextIllegalArgumentExceptionPropertyEditorsetAsTextPropertyEditor.setAsText()setAsTextIllegalArgumentExceptionerrors.propertiesPropertyEditor.setAsText()IllegalArgumentException

有解决方案吗?重复一遍,我想要的是将生成的特定错误消息呈现到Spring表单上的错误消息中。我能想到的唯一替代方法是将SSN存储为命令中的文本,并在验证器中执行验证。文本到 SSN 对象的转换将在表单的 .这不太可取,因为我的窗体(和模型)具有许多属性,并且我不想创建和维护将每个模型属性作为文本字段的命令。PropertyEditoronSubmit

以上只是一个例子,我的实际代码不是Person / SSN,所以没有必要回复“为什么不将SSN存储为文本......”


答案 1

您正在尝试在活页夹中执行验证。这不是粘合剂的目的。绑定器应该将请求参数绑定到您的支持对象,仅此而已。属性编辑器将 Strings 转换为对象,反之亦然 - 它不是为执行任何其他操作而设计的。

换句话说,你需要考虑关注点的分离 - 你试图将功能硬塞进一个对象中,而这个对象除了将字符串转换为对象之外,从来不打算做任何事情,反之亦然。

您可以考虑将 SSN 对象分解为多个易于绑定的可验证字段(字符串对象、日期等基本对象等)。这样,您可以在绑定后使用验证程序来验证 SSN 是否正确,也可以直接设置错误。使用属性编辑器,您抛出一个 IllegalArgumentException,Spring 将其转换为类型不匹配错误,因为这就是它 - 字符串与预期的类型不匹配。这就是它的全部。另一方面,验证者可以做到这一点。您可以使用 spring 绑定标记绑定到嵌套字段,只要填充了 SSN 实例 - 必须先使用 new() 对其进行初始化。例如:

<spring:bind path="ssn.firstNestedField">...</spring:bind>

但是,如果您确实想在此路径上持久存在,请让您的属性编辑器保留一个错误列表 - 如果要抛出非法参数异常,请将其添加到列表中,然后抛出 IllegalArgumentException(如果需要,请捕获并重新删除)。因为您可以在与绑定相同的线程中构造属性编辑器,所以如果只是重写属性编辑器默认行为 ,则需要找到它用于执行绑定的钩子,并重写它 - 执行您现在正在执行的相同属性编辑器注册(除了在相同的方法中,以便您可以保留对编辑器的引用),然后在绑定结束时,它将是线程安全的, 如果您提供公共访问器,则可以通过从编辑器中检索列表来注册错误。检索到列表后,您可以对其进行处理并相应地添加错误。


答案 2

如所说:

我想要的是属性编辑器生成的特定错误消息,以显示在Spring表单上的错误消息中

在幕后,Spring MVC 使用 BindingErrorProcessor 策略来处理缺少的字段错误,并将 PropertyAccessException 转换为 FieldError。因此,如果要覆盖默认的 Spring MVC BindingErrorProcessor 策略,则必须根据以下条件提供 BindingErrorProcessor 策略:

public class CustomBindingErrorProcessor implements DefaultBindingErrorProcessor {

    public void processMissingFieldError(String missingField, BindException errors) {
        super.processMissingFieldError(missingField, errors);
    }

    public void processPropertyAccessException(PropertyAccessException accessException, BindException errors) {
        if(accessException.getCause() instanceof IllegalArgumentException)
            errors.rejectValue(accessException.getPropertyChangeEvent().getPropertyName(), "<SOME_SPECIFIC_CODE_IF_YOU_WANT>", accessException.getCause().getMessage());
        else
            defaultSpringBindingErrorProcessor.processPropertyAccessException(accessException, errors);
    }

}

为了进行测试,让我们执行以下操作

protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) {
    binder.registerCustomEditor(SSN.class, new PropertyEditorSupport() {

        public String getAsText() {
            if(getValue() == null)
                return null;

            return ((SSN) getValue()).toString();
        }

        public void setAsText(String value) throws IllegalArgumentException {
            if(StringUtils.isBlank(value))
                return;

            boolean somethingGoesWrong = true;
            if(somethingGoesWrong)
                throw new IllegalArgumentException("Something goes wrong!");
        }

    });
}

现在我们的测试类

public class PersonControllerTest {

    private PersonController personController;
    private MockHttpServletRequest request;

    @BeforeMethod
    public void setUp() {
        personController = new PersonController();
        personController.setCommandName("command");
        personController.setCommandClass(Person.class);
        personController.setBindingErrorProcessor(new CustomBindingErrorProcessor());

        request = new MockHttpServletRequest();
        request.setMethod("POST");
        request.addParameter("ssn", "somethingGoesWrong");
    }

    @Test
    public void done() {
        ModelAndView mav = personController.handleRequest(request, new MockHttpServletResponse());

        BindingResult bindingResult = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + "command");

        FieldError fieldError = bindingResult.getFieldError("ssn");

        Assert.assertEquals(fieldError.getMessage(), "Something goes wrong!");
    }

}

问候


推荐