Spring проверка, как заставить PropertyEditor генерировать определенное сообщение об ошибке

Я использую 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 - это обычная java.beans.PropertyEditor, которая может преобразовывать текст в объект SSN:

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 throws IllegalArgumentException, Spring просто отображает общее сообщение об ошибке, определенное в errors.properties. То, что я хочу, является конкретным сообщением об ошибке, которое зависит от точной причины, по которой введенный SSN недействителен. PropertyEditor.setAsText() будет определять причину. Я попытался встроить текст причины ошибки в текст IllegalArgumentException, но Spring просто рассматривает его как общую ошибку.

Есть ли решение? Повторяю то, что я хочу, это конкретное сообщение об ошибке, сгенерированное PropertyEditor для отображения сообщения об ошибке в форме Spring. Единственной альтернативой, о которой я могу думать, является сохранение SSN в виде текста в команде и выполнение проверки в валидаторе. Преобразование текста в объект SSN будет иметь вид onSubmit. Это менее желательно, так как моя форма (и модель) имеет много свойств, и я не хочу создавать и поддерживать команду, которая имеет атрибут каждой модели как текстовое поле.

Вышеприведенный пример - мой фактический код не является Person/SSN, поэтому нет необходимости отвечать "почему бы не хранить SSN как текст..."

Ответ 1

Вы пытаетесь выполнить проверку в связующем. Это не связующее назначение. Связывание должно связывать параметры запроса с вашим фоновым объектом, не более того. Редактор свойств преобразует строки в объекты и наоборот - он не предназначен для чего-либо другого.

Другими словами, вам нужно рассмотреть вопрос о разделении проблем - вы пытаетесь использовать функции shoehorn в объекте, который никогда не должен был делать ничего, кроме преобразования строки в объект и наоборот.

Вы можете рассмотреть возможность разбиения вашего объекта SSN на несколько проверяемых полей, которые легко связаны (объекты String, базовые объекты, такие как Dates и т.д.). Таким образом, вы можете использовать валидатор после привязки, чтобы проверить правильность SSN, или вы можете установить ошибку напрямую. С редактором свойств вы бросаете IllegalArgumentException, Spring преобразует его в ошибку несоответствия типа, потому что это то, что это - строка не соответствует ожидаемому типу. Это все, что есть. С другой стороны, валидатор может это сделать. Вы можете использовать тег связывания Spring для привязки к вложенным полям, если экземпляр SSN заполнен - ​​он должен быть сначала инициализирован с помощью new(). Например:

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

Если вы действительно хотите сохранить этот путь, сохраните список ошибок в вашем редакторе свойств, если он должен бросить исключение IllegalArgumentException, добавить его в список и затем выбросить исключение IllegalArgumentException (при необходимости поймать и реконструировать), Поскольку вы можете построить свой редактор свойств в том же потоке, что и привязка, он будет потокобезопасным, если вы просто переопределите поведение по умолчанию редактора свойств - вам нужно найти крючок, который он использует для выполнения привязки, и переопределить его - сделать тот же редактор свойств (за исключением того же метода, чтобы вы могли сохранить ссылку на свой редактор), а затем в конце привязки вы можете регистрировать ошибки, получая список из вашего редактора, если вы предоставляете публичный доступ, После того, как список будет восстановлен, вы можете обработать его и соответственно добавить свои ошибки.

Ответ 2

Как сказано:

То, что я хочу, это сообщение об ошибке, сгенерированное 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!");
    }

}

С уважением,

Ответ 3

В качестве ответа на вопрос @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 в этой версии.

Ответ 4

Это похоже на проблему, с которой я столкнулся с NumberFormatExceptions, когда значение для свойства integer не могло быть связано, если, скажем, строка была введена в форму. Сообщение об ошибке в форме было общим сообщением для этого исключения.

Решение заключалось в том, чтобы добавить мой собственный пакет ресурсов сообщений в контекст моего приложения и добавить собственное сообщение об ошибке для несоответствий типов для этого свойства. Возможно, вы можете сделать что-то подобное для IllegalArgumentExceptions в определенном поле.

Ответ 5

Я думаю, вы могли бы просто попытаться поместить это в свой источник сообщения:

typeMismatch.person.ssn = Неверный формат SSN