Как в spring написать валидатор, использующий коды сообщений, и не забыть кого-то из них

April 28, 2008

Введение в суть вопроса



Я продолжаю выкладывать на всеобщее обозрение ряд своих утилиток помогающих и упрощающих разработку. Сегодня я расскажу об инструменте, который наверняка пригодится всем тем кто пользуется spring. Предположим что вы создаете некоторый контроллер, например, такой:
  1. <bean name="loginController" class="blz.server.controllers.LoginController">
  2.         <property name="requireSession" value="true"/>
  3.         <property name="supportedMethods" value="GET,POST"/>
  4.         <property name="formView" value="loginStarted"/>
  5.         <property name="commandClass" value="blz.model.dto.LoginDTO"/>
  6.         <property name="commandName" value="login"/>
  7.         <property name="validator">
  8.             <bean class="blz.model.dto.validators.ValidateLoginDTO"/>
  9.         </property>
  10.     </bean>
Как видите здесь я задекларировал, что входным параметром к контроллеру будет класс "blz.model.dto.LoginDTO". Для проверки того поля в форме были указаны верно, без ошибок, в spring используется validator, на примере это, blz.model.dto.validators.ValidateLoginDTO.

Сам код класса валидатора может выглядеть примерно так:
  1. public class ValidateLoginDTO implements Validator {
  2.  
  3.     public boolean supports(Class clazz) {
  4.         return clazz.isAssignableFrom(LoginDTO.class);
  5.     }
  6.  
  7.     public void validate(Object target, Errors errors) {
  8.         ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USEREMAIL, "validation.failed.noemail.for.login");
  9.         ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USERPASSWORD, "validation.failed.nopass.for.login");
  10.     }
  11. }
Основное внимание на вызов метода "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:
  1. <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
  2.         <property name="basenames">
  3.             <list>
  4.                 <value>/WEB-INF/localization/register_and_login</value>
  5.             </list>
  6.         </property>
  7.         <property name="defaultEncoding" value="windows-1251"/>
  8.         <property name="fallbackToSystemLocale" value="true"/>
  9.   </bean>
На этом все.

Надо сказать что подобный многошаговый процесс меня сильно раздражает т.к. способствует появлению ошибок в основном из-за неаккуратности или спешки (ну-ну, разве может быть иначе?). Несмотря на это изменить шаги, убрать, поменять местами нельзя, т.к. ничего лучшего не придумывается, а если даже и придумается то врядли этот самокат будет плавно входить в общую систему spring. Однако это не значит, что нельзя упростить для себя любимого процесс проверки наличия в файла свойств нужных для работы валидаторов кодов сообщений. Для этого я решил добавить поддержку специальной аннотации. Вот ее код:
  1. /**
  2.  * Аннотация для маркировки тех классов, которые хотят использовать
  3.  * услуги проверки их на наличие корректных кодов сообщений
  4.  */
  5.  
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Target(ElementType.TYPE)
  8. public @interface ValidationBundle {
  9.     /**
  10.      * Базовое имя ResourceBundle
  11.      * @return
  12.      */
  13.     String value();
  14.  
  15.     /**
  16.      * Список локалей, для которых нужно выполнить проверку
  17.      * @return
  18.      */
  19.     String[] locales () default {};
  20.  
  21.  
  22.     /**
  23.      * Кодировка файла с ресурсами, ну так получилось, что делать ...
  24.      * @return
  25.      */
  26.     String encoding () default "ISO8859-1";
  27.  
  28.  
  29.  
  30.     @Retention(RetentionPolicy.RUNTIME)
  31.     @Target({ElementType.FIELD} )
  32.  
  33.     /**
  34.      * Еще один маркер, но уже не для класса, а для конкретных его полей 
  35.      */
  36.     public static @interface Marker {}
  37. }
Теперь я вернусь к файлу валидатора и немного его перепишу:
  1. @SuppressWarnings ({"inchecked", "unsafe"})
  2. @ValidationBundle(value="/WEB-INF/localization/register_and_login", locales = {"RU_ru"}, encoding="windows-1251")
  3. public class ValidateLoginDTO implements Validator {
  4.  
  5.     @ValidationBundle.Marker
  6.     private static final String VALIDATION_FAILED_NOEMAIL_FOR_LOGIN = "validation.failed.noemail.for.login";
  7.     @ValidationBundle.Marker
  8.     private static final String VALIDATION_FAILED_NOPASS_FOR_LOGIN = "validation.failed.nopass.for.login";
  9.  
  10.     public boolean supports(Class clazz) {
  11.         return clazz.isAssignableFrom(LoginDTO.class);
  12.     }
  13.  
  14.     public void validate(Object target, Errors errors) {
  15.         ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USEREMAIL, VALIDATION_FAILED_NOEMAIL_FOR_LOGIN);
  16.         ValidationUtils.rejectIfEmptyOrWhitespace(errors, LoginDTO.F_USERPASSWORD, VALIDATION_FAILED_NOPASS_FOR_LOGIN);
  17.     }
  18. }
Как видите изменений немного: в самом начале файла я выполнил маркировку класса валидатора как использующего коды сообщений расположенные внутри ResourceBundle с базовым именем "/WEB-INF/localization/register_and_login". Указал для каких локалей необходимо выполнить сканирование, и указал кодировку файла с ресурсами (дело в том, что родные для java ResourceBundle загружают данные из файлов в кодировке ISO8859-1, а для spring можно явно указать кодировку, например, windows-1251 и это гораздо удобнее чем мучаться с утилитой native2ascii.exe).

Все поля хранящие в себе значение кода сообщения я промаркировал аннотацией "@ValidationBundle.Marker".

Что дальше?

Теперь есть два варианта как запустить проверку spring-контекста и ваших классов на корректность. Во-первых, и это наиболее удобный для меня способ это создать ant-скрипт, частью которого является создание отчета об найденных ошибках. Для этого я создал ant-скрипт.
  1. <?xml version="1.0"?>
  2. <project default="makeanotest">
  3.  
  4.  
  5.     <target name="makeanotest">
  6.  
  7.         <path id="library.spring.classpath">
  8.             <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/dist">
  9.                 <include name="**/*.jar"/>
  10.             </fileset>
  11.         </path>
  12.  
  13.         <path id="library.velocity.classpath">
  14.             <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/velocity">
  15.                 <include name="**/*.jar"/>
  16.             </fileset>
  17.         </path>
  18.  
  19.         <path id="library.spring-all-libs.classpath">
  20.             <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/">
  21.                 <include name="**/*.jar"/>
  22.             </fileset>
  23.         </path>
  24.  
  25.  
  26.         <path id="library.ant.classpath">
  27.             <fileset dir="E:/Program_Files_2/Libraries/ant/lib/">
  28.                 <include name="**/*.jar"/>
  29.             </fileset>
  30.         </path>
  31.  
  32.  
  33.         <path id="library.commons.classpath">
  34.             <fileset dir="E:/Program_Files_2/Libraries/spring-framework-2.5/lib/jakarta-commons/">
  35.                 <include name="**/*.jar"/>
  36.             </fileset>
  37.         </path>
  38.  
  39.         <dirname property="base" file="${ant.file}"/>
  40.  
  41.         <path id="project.sourcepath">
  42.             <dirset dir="${base}/src">
  43.                 <include name="**/*.java"/>
  44.                 <exclude name="com/**"/>
  45.                 <exclude name="org/**"/>
  46.                 <exclude name="javax/**"/>
  47.             </dirset>
  48.         </path>
  49.  
  50.  
  51.         <path id="project.classpath" location="${base}/out/production/sitegraph/"/>
  52.  
  53.         <path id="all.classpath">
  54.             <path refid="library.spring.classpath"/>
  55.             <path refid="library.spring-all-libs.classpath"/>
  56.             <path refid="library.ant.classpath"/>
  57.             <path refid="project.classpath"/>
  58.         </path>
  59.  
  60.         <!--
  61.         Создаю новую цель
  62.         -->
  63.         <taskdef name="scananno"
  64.                  classname="blz.server.utils.anno.AntTaskAnnoScan"
  65.                  classpathref="all.classpath"
  66.                 />
  67.  
  68.         <property name="browser" location=
  69.                 "C:/Program Files/Internet Explorer/iexplore.exe"/>
  70.  
  71.         <property name="file-1" location="${base}/out/report-anno-1.html"/>
  72.         <property name="file-2" location="${base}/out/report-anno-2.html"/>
  73.  
  74.         <!--
  75.         Вот пример использования новой задачи.
  76.         В качестве параметров я указываю будет ли завершаться скрипт ant аварийно, если
  77.         все нужные ресурсы не были найдены
  78.         далее я указываю путь к файлу, куда должен быть помещен результат в виде html-отчета
  79.         затем я указываю путь к исходному файлу spring
  80.         и наконец признак того как будут запрашиваться нужные ресурсы
  81.         в случае если признак useContext равен true то ресурсы будут загружаться из самого spring
  82.         -->
  83.         <scananno
  84.                 failOnErrors="false"
  85.                 targetFileName="${file-1}"
  86.                 pathToSpringFile="${base}/web/WEB-INF/spring-tester.xml"
  87.                 useContext="false"
  88.                 pathToBaseResourcesDir="${base}/web/"
  89.                 />
  90.         <!--
  91.         Теперь, после создания отчета, нужно его показать в браузере
  92.         -->
  93.         <exec executable="${browser}" spawn="true">
  94.             <arg value="${file-1}"/>
  95.         </exec>
  96.  
  97.         <!--
  98.         В этом случае к параметрам показанным в прошлом примере добавляются еще два:
  99.         признак того что загрузка должна вестись не из контекста а напрямую с помощью ResourceBundle
  100.         а также путь к корню каталога относительно которого будут искаться файлы properties
  101.         -->
  102.         <scananno
  103.                 failOnErrors="true"
  104.                 targetFileName="${file-2}"
  105.                 pathToSpringFile="${base}/web/WEB-INF/spring-tester.xml"
  106.                 useContext="false"
  107.                 pathToBaseResourcesDir="${base}/web/"
  108.                 />
  109.  
  110.     </target>
  111. </project>
Как видите, я создал новую пользовательскую задачу - scananno - передав ей в качестве параметров путь к файлу, куда должен быть помещен результат в виде html-отчета, затем путь к исходному файлу spring и, наконец, признак того как будут запрашиваться нужные ресурсы (подробнее об этих режимах рассказано в комментариях).

После запуска сценария я получу такой html-отчет:



Надо сказать, что внешний вид отчета можно гибко настраивать: в файлах с исходниками проекта вы найдете velocity-шаблон для выходного отчета (если вы не знаете что такое velocity, то скоро я опубликую серию статей посвященную ему).

Теперь про второй вариант использования. В этом случае в контекст spring добавляется новый bean, который зарегистрирован в случае получения события "Контекст обновлен" выполнить его сканирование и сформировать отчет.
  1. <bean id="annoBean" class="blz.server.utils.anno.AnnoBean">
  2.         <constructor-arg index="0" value="report-anno.html" />
  3.    </bean>
При запуске контекста, автоматически будет вызван этот bean, который и сформирует html-файл с отчетом.

В качестве единственного параметра к конструктору нужно передать путь к файлу, куда будет помещен сформированный html-отчет об найденных ошибках. В случае если путь не абсолютный, а относительный (не начинается с file:), то нужно каким-то образом сообщить об "базе" файла. Класс бина (AnnoBean) реализует интерфейс ServletContextAware. А значит что если все пошло хорошо то ему сообщат о местоположении корня веб-приложения и относительный путь будет отсчитываться от него.

Исходники



https://github.com/study-and-dev-site-attachments/all-in-one/tree/master/java/annoforspringmessages

Хотя код утилиты заточен под работу с валидаторами, однако довольно легко можно ее перестроить и научить выполнять проверки наличия кодов сообщений для произвольный spring-бинов.