Как в spring написать валидатор использующий коды сообщений и не забыть кого-то из них
Введение в суть вопроса
Я продолжаю выкладывать на всеобщее обозрение ряд своих утилиток помогающих и упрощающих разработку. Сегодня я расскажу об инструменте, который наверняка пригодится всем тем кто пользуется spring. Предположим что вы создаете некоторый контроллер, например, такой:
<bean name="loginController" class="blz.server.controllers.LoginController"> <property name="requireSession" value="true"/> <property name="supportedMethods" value="GET,POST"/> <property name="formView" value="loginStarted"/> <property name="commandClass" value="blz.model.dto.LoginDTO"/> <property name="commandName" value="login"/> <property name="validator"> <bean class="blz.model.dto.validators.ValidateLoginDTO"/> </property> </bean>
Как видите здесь я задекларировал, что входным параметром к контроллеру будет класс "blz.model.dto.LoginDTO". Для проверки того поля в форме были указаны верно, без ошибок, в spring используется validator, на примере это, blz.model.dto.validators.ValidateLoginDTO.
Сам код класса валидатора может выглядеть примерно так:
public class ValidateLoginDTO implements Validator {
public boolean supports(Class clazz) {
return clazz.isAssignableFrom(LoginDTO.class);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USEREMAIL, "validation.failed.noemail.for.login");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USERPASSWORD, "validation.failed.nopass.for.login");
}
}
Основное внимание на вызов метода "ValidationUtils.rejectIfEmptyOrWhitespace". Здесь я проверяю внутри входного объекта-комманды условие, такое чтобы значение поля email и пароль были заполненными. В случае если это не так, то в объект Errors помещается код ошибки "validation.failed.noemail.for.login" и "validation.failed.nopass.for.login".
Эти коды не берутся из ниоткуда: я должен создать файл properties, например, "/WEB-INF/localization/register_and_login.properties" и поместить в него следующий текст:
validation.failed.noemail.for.login=Ошибка, вы не указали ваш email. Вход в систему не возможен.
validation.failed.nopass.for.login=Ошибка, вы не указали ваш пароль. Вход в систему не возможен.
Это еще не все, теперь последний шаг - регистрация ресурсов внутри контекста spring:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basenames"> <list> <value>/WEB-INF/localization/register_and_login</value> </list> </property> <property name="defaultEncoding" value="windows-1251"/> <property name="fallbackToSystemLocale" value="true"/> </bean>
На этом все.
Надо сказать что подобный многошаговый процесс меня сильно раздражает т.к. способствует появлению ошибок в основном из-за неаккуратности или спешки (ну-ну, разве может быть иначе?). Несмотря на это изменить шаги, убрать, поменять местами нельзя, т.к. ничего лучшего не придумывается, а если даже и придумается то врядли этот самокат будет плавно входить в общую систему spring. Однако это не значит, что нельзя упростить для себя любимого процесс проверки наличия в файла свойств нужных для работы валидаторов кодов сообщений. Для этого я решил добавить поддержку специальной аннотации. Вот ее код:
/**
* Аннотация для маркировки тех классов, которые хотят использовать услуги
* проверки их на наличие корректных кодов сообщений
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ValidationBundle {
/**
* Базовое имя ResourceBundle
*
* @return
*/
String value();
/**
* Список локалей, для которых нужно выполнить проверку
*
* @return
*/
String[] locales () default {};
/**
* Кодировка файла с ресурсами, ну так получилось, что делать ...
*
* @return
*/
String encoding () default "ISO8859-1";
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD} )
/**
* Еще один маркер, но уже не для класса, а для конкретных его полей
*/
public static @interface Marker {}
}
Теперь я вернусь к файлу валидатора и немного его перепишу:
@SuppressWarnings ({"inchecked", "unsafe"})
@ValidationBundle(value="/WEB-INF/localization/register_and_login", locales = {"RU_ru"}, encoding="windows-1251")
public class ValidateLoginDTO implements Validator {
@ValidationBundle.Marker
private static final String VALIDATION_FAILED_NOEMAIL_FOR_LOGIN = "validation.failed.noemail.for.login";
@ValidationBundle.Marker
private static final String VALIDATION_FAILED_NOPASS_FOR_LOGIN = "validation.failed.nopass.for.login";
public boolean supports(Class clazz) {
return clazz.isAssignableFrom(LoginDTO.class);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USEREMAIL, VALIDATION_FAILED_NOEMAIL_FOR_LOGIN);
ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USERPASSWORD, VALIDATION_FAILED_NOPASS_FOR_LOGIN);
}
}
Как видите изменений немного: в самом начале файла я выполнил маркировку класса валидатора как использующего коды сообщений расположенные внутри ResourceBundle с базовым именем "/WEB-INF/localization/register_and_login". Указал для каких локалей необходимо выполнить сканирование, и указал кодировку файла с ресурсами (дело в том, что родные для java ResourceBundle загружают данные из файлов в кодировке ISO8859-1, а для spring можно явно указать кодировку, например, windows-1251 и это гораздо удобнее чем мучаться с утилитой native2ascii.exe).
Все поля хранящие в себе значение кода сообщения я промаркировал аннотацией "@ValidationBundle.Marker".
Что дальше?
Теперь есть два варианта как запустить проверку spring-контекста и ваших классов на корректность. Во-первых, и это наиболее удобный для меня способ это создать ant-скрипт, частью которого является создание отчета об найденных ошибках. Для этого я создал ant-скрипт.
<?xml version="1.0"?> <project default="makeanotest"> <target name="makeanotest"> <path id="library.spring.classpath"> <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/dist"> <include name="**/*.jar"/> </fileset> </path> <path id="library.velocity.classpath"> <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/velocity"> <include name="**/*.jar"/> </fileset> </path> <path id="library.spring-all-libs.classpath"> <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/"> <include name="**/*.jar"/> </fileset> </path> <path id="library.ant.classpath"> <fileset dir="E:/Program_Files_2/Libraries/ant/lib/"> <include name="**/*.jar"/> </fileset> </path> <path id="library.commons.classpath"> <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/jakarta-commons/"> <include name="**/*.jar"/> </fileset> </path> <dirname property="base" file="${ant.file}"/> <path id="project.sourcepath"> <dirset dir="${base}/src"> <include name="**/*.java"/> <exclude name="com/**"/> <exclude name="org/**"/> <exclude name="javax/**"/> </dirset> </path> <path id="project.classpath" location="${base}/out/production/sitegraph/"/> <path id="all.classpath"> <path refid="library.spring.classpath"/> <path refid="library.spring-all-libs.classpath"/> <path refid="library.ant.classpath"/> <path refid="project.classpath"/> </path> <!-- Создаю новую цель --> <taskdef name="scananno" classname="blz.server.utils.anno.AntTaskAnnoScan" classpathref="all.classpath" /> <property name="browser" location= "C:/Program Files/Internet Explorer/iexplore.exe"/> <property name="file-1" location="${base}/out/report-anno-1.html"/> <property name="file-2" location="${base}/out/report-anno-2.html"/> <!-- Вот пример использования новой задачи. В качестве параметров я указываю будет ли завершаться скрипт ant аварийно, если все нужные ресурсы не были найдены далее я указываю путь к файлу, куда должен быть помещен результат в виде html-отчета затем я указываю путь к исходному файлу spring и наконец признак того как будут запрашиваться нужные ресурсы в случае если признак useContext равен true то ресурсы будут загружаться из самого spring --> <scananno failOnErrors="false" targetFileName="${file-1}" pathToSpringFile="${base}/web/WEB-INF/spring-tester.xml" useContext="false" pathToBaseResourcesDir="${base}/web/" /> <!-- Теперь, после создания отчета, нужно его показать в браузере --> <exec executable="${browser}" spawn="true"> <arg value="${file-1}"/> </exec> <!-- В этом случае к параметрам показанным в прошлом примере добавляются еще два: признак того что загрузка должна вестись не из контекста а напрямую с помощью ResourceBundle а также путь к корню каталога относительно которого будут искаться файлы properties --> <scananno failOnErrors="true" targetFileName="${file-2}" pathToSpringFile="${base}/web/WEB-INF/spring-tester.xml" useContext="false" pathToBaseResourcesDir="${base}/web/" /> </target> </project>
Как видите, я создал новую пользовательскую задачу - scananno - передав ей в качестве параметров путь к файлу, куда должен быть помещен результат в виде html-отчета, затем путь к исходному файлу spring и, наконец, признак того как будут запрашиваться нужные ресурсы (подробнее об этих режимах рассказано в комментариях).
После запуска сценария я получу такой html-отчет:
Надо сказать, что внешний вид отчета можно гибко настраивать: в файлах с исходниками проекта вы найдете velocity-шаблон для выходного отчета (если вы не знаете что такое velocity, то скоро я опубликую серию статей посвященную ему).
Теперь про второй вариант использования. В этом случае в контекст spring добавляется новый bean, который зарегистрирован в случае получения события "Контекст обновлен" выполнить его сканирование и сформировать отчет.
<bean id="annoBean" class="blz.server.utils.anno.AnnoBean"> <constructor-arg index="0" value="report-anno.html" /> </bean>
При запуске контекста, автоматически будет вызван этот bean, который и сформирует html-файл с отчетом.
В качестве единственного параметра к конструктору нужно передать путь к файлу, куда будет помещен сформированный html-отчет об найденных ошибках. В случае если путь не абсолютный, а относительный (не начинается с file:), то нужно каким-то образом сообщить об "базе" файла. Класс бина (AnnoBean) реализует интерфейс ServletContextAware. А значит что если все пошло хорошо то ему сообщат о местоположении корня веб-приложения и относительный путь будет отсчитываться от него.