Spring验证,如何让PropertyEditor生成特定的错误消息

时间:2009-03-27 23:17:37

标签: java validation spring

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

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

并将Person包装在Spring表单编辑命令中:

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

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

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
    }
}

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

有解决方案吗?重复一遍,我想要的是PropertyEditor生成的特定错误消息,表明Spring表单上的错误消息。我能想到的唯一选择是将SSN作为文本存储在命令中,并在验证器中执行验证。 SSN对象转换的文本将以onSubmit的形式进行。这是不太理想的,因为我的表单(和模型)有很多属性,我不想创建和维护一个将每个模型属性作为文本字段的命令。

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

5 个答案:

答案 0 :(得分:6)

您正尝试在活页夹中进行验证。这不是活页夹的目的。绑定器应该将请求参数绑定到您的后备对象,仅此而已。属性编辑器将字符串转换为对象,反之亦然 - 它不是为了做任何其他事情而设计的。

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

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

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

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

答案 1 :(得分:5)

如上所述:

  

我想要的是由PropertyEditor 生成的特定错误消息,以显示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!");
    }

}

的问候,

答案 2 :(得分:1)

作为@Arthur Ronald回答的后续行动,这就是我最终实现这一目标的方式:

在控制器上:

setBindingErrorProcessor(new CustomBindingErrorProcessor());

然后是绑定错误处理器类:

public class CustomBindingErrorProcessor extends DefaultBindingErrorProcessor {

    public void processPropertyAccessException(PropertyAccessException accessException, 
                                               BindingResult bindingResult) {

        if(accessException.getCause() instanceof IllegalArgumentException){

            String fieldName = accessException.getPropertyChangeEvent().getPropertyName();
            String exceptionError = accessException.getCause().getMessage();

            FieldError fieldError = new FieldError(fieldName,
                                                   "BINDING_ERROR", 
                                                   fieldName + ": " + exceptionError);

            bindingResult.addError(fieldError);
        }else{
            super.processPropertyAccessException(accessException, bindingResult);
        }

    }

}        

因此处理器方法的签名在此版本上采用BindingResult而不是BindException。

答案 3 :(得分:0)

这听起来类似于我在使用NumberFormatExceptions时遇到的问题,如果在表单中输入了String,则无法绑定整数属性的值。表单上的错误消息是该异常的通用消息。

解决方案是将我自己的消息资源包添加到我的应用程序上下文中,并为该属性上的类型不匹配添加我自己的错误消息。也许你可以为特定领域的IllegalArgumentExceptions做类似的事情。

答案 4 :(得分:0)

我相信你可以尝试将它放在你的消息来源中:

typeMismatch.person.ssn =错误的SSN格式