Spring MVC кастомная аннотация для валидации форм

Вы наверное не раз встречали на сайтах валидаторы форм, которые следят за тем, что бы вы ввели корректную информацию. Но как же сделать валидацию под свои нужды? Именно этот вопрос будет рассматриваться в данном уроке.

Шаг 0

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

Допустим у нас есть сущность User, который должен дать нам свое Имя, номер телефона и указать год рождения. Для этого мы создадим две аннотации с помощью которых и будем выполнять валидацию данных.

Шаг 1

Начнем с простого. Давайте создадим Spring MVC проект, для этого в Intellij IDEA открываем File -> New Projject -> Spring MVC, у вас появится следующая структура проекта:

Вот у нас и готов проект на который мы будем привязывать форму и её валидацию.

Шаг 2

Теперь добавим нужные нам зависимости в pom.xml:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.0.1.Final</version>
</dependency>

<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

После этого можно приступать к созданию кастомного валидатора.

Шаг 3

Создаем сущность User:

package com.devcolibri.mvc.entity;

import com.devcolibri.mvc.validator.Phone;
import com.devcolibri.mvc.validator.Year;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.Past;
import java.util.Date;

public class User {

    private String name;

    @DateTimeFormat(pattern="MM/dd/yyyy")
    @Past
    @Year(2000)
    private Date date;

    @Phone
    private String phone;

    public User(String name, Date date, String phone) {
        this.name = name;
        this.date = date;
        this.phone = phone;
    }

    public User() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

}

И немного позже на эту сущность мы будем добавлять аннотации, которые и будут выполнять валидацию данных в экземпляре этой сущности.

Шаг 4

Создаем аннотацию Phone:

У вас появится Phone.java, дальше вы должны в нем написать следующий код:

package com.devcolibri.mvc.validator;

import javax.validation.Constraint;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PhoneConstraintValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
    String message() default "{Phone}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Приведенный выше код в имеет три метода. Эти три метода требуются JSR-303 спец. Если бы наша аннотация принимала любые аргументы, мы бы определили их в теле аннотации, как методы. Мы будем рассматривать этот пример в реализации следующей аннотации.

В message() методе мы можем указать сообщение которое будет выводится при валидации, мы это будем указывать с помощью Spring resource bundle.

Шаг 5

Немного теории!

Как вы видите саму аннотацию мы аннотируем еще 4-мя аннотациями:

@Documented — указывает, что помеченная таким образом аннотация должна быть добавлена в javadoc поля/метода.

@Target — указывает, что именно мы можем пометить этой аннотацией.

ElementType.PACKAGE — только для пакетов;
ElementType.TYPE — только для классов;
ElementType.CONSTRUCTOR — только для конструкторов;
ElementType.METHOD — только для методов;
ElementType.FIELD — только для атрибутов(переменных) класса;
ElementType.PARAMATER — только для параметров метода;
ElementType.LOCAL_VARIABLE — только для локальных переменных.

@Retention — позволяет указать жизненный цикл аннотации: будет она присутствовать только в исходном коде, в скомпилированном файле, или она будет также видна и в процессе выполнения.

RetentionPolicy.CLASS — будет присутствовать в скомпилированном файле;
RetentionPolicy.RUNTIME — будет присутствовать только в момент выполнения;
RetentionPolicy.SOURCE — будет присутствовать только в исходном коде.

@Constraint — список реализаций данного интерфейса.

Шаг 6

Теперь создаем реализацию данной аннотации которая указанна в аннотации @Constraint, создаем простой класс с именем PhoneConstraintValidator.java со следующим содержимым:

package com.devcolibri.mvc.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PhoneConstraintValidator implements ConstraintValidator<Phone, String> {

    @Override
    public void initialize(Phone phone) {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public boolean isValid(String phoneField, ConstraintValidatorContext cxt) {
        if(phoneField == null) {
            return false;
        }
        return phoneField.matches("[0-9()-\\.]*");
    }

}

Давайте посмотрим на приведенный выше код. Как вы видите мы реализуем интерфейс ConstraintValidator, который принимает два типа: тип аннотации который будет поддерживаться, и тип свойства, который он проверяет (в данном примере, телефон, String).

initialize(Phone phone) — этот метод пустой и не имеет реализации, но он может быть использован, для того чтобы сохранить данные из аннотации, во второй аннотации нам пригодится этот метод и мы его реализуем.

А основную логику проверки выполняет метод isValid(String phoneField, ConstraintValidatorContext cxt). Значение поля передается в качестве первого аргумента, как вы можете видеть, я просто проверил, что номер телефона содержатся только цифры, круглые скобки и тире.

Теперь можно проаннотировать поле PhoneNumber в сущности User но мы сделаем это позже.

Шаг 7

Теперь создадим аннотацию @Year:

package com.devcolibri.mvc.validator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = YearConstraintValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Year {

    int value();
    String message() default "{Year}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

Обратите внимание на value() метод, который мы будем использовать, чтобы проверить год. Остальная часть кода — это основной шаблон.

Шаг 8

Давайте теперь напишем реализацию данной аннотации. Для этого создаем класс YearConstraintValidator.java со следующим содержимым:

package com.devcolibri.mvc.validator;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Calendar;
import java.util.Date;

public class YearConstraintValidator implements ConstraintValidator<Year, Date> {

    private int annotationYear;

    @Override
    public void initialize(Year year) {
        this.annotationYear = year.value();
    }

    @Override
    public boolean isValid(Date target, ConstraintValidatorContext cxt) {
        if(target == null) {
            return true;
        }
        Calendar c = Calendar.getInstance();
        c.setTime(target);
        int fieldYear = c.get(Calendar.YEAR);

        return fieldYear == annotationYear;
    }

}

Первое на что нужно обратить внимание это то что теперь мы используем метод initialize(Year year) так как он выполняется первым, мы присваиваем локальной переменной значение с аннотации @Year.

После этого выполняется метод isValid(Date target, ConstraintValidatorContext cxt) метод, который будет проверять на валидность дату, то есть если пользователем будет неправильно указанна дату то вернется false, что будет значить не валидность даты.

Шаг 9

Теперь создаем конфигурационный класс WebConfig.java со следующим содержимым:

package com.devcolibri.mvc.springconfig;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages="com.devcolibri.mvc")
public class WebConfig {

    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/pages/");
        resolver.setSuffix(".jsp");
        return resolver;
    }

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        return messageSource;
    }

}

И сразу же для MessageSource создаем проперти файл в resources/messages.properties:

Past=Date must be in the past
Year=Year must be 2000
Phone=Invalid characters in phone number
typeMismatch=Invalid format

И файл web.xml который должен находится по пути webapp/WEB-INF/web.xml, должен содержать следующее:

<web-app version="2.4"
	xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
	http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

	<display-name>Spring MVC Application</display-name>

    <servlet>
		<servlet-name>mvc-dispatcher</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
               org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.devcolibri.mvc.springconfig</param-value>
        </init-param>
	</servlet>

	<servlet-mapping>
		<servlet-name>mvc-dispatcher</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
</web-app>

Шаг 10

В папке webapp/WEB-INF/pages/ создаем hello.jsp со следующим содержимым:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<!DOCTYPE HTML>
<html>
<head>
    <style>
        body { background-color: #eee; font: helvetica; }
        #container { width: 500px; background-color: #fff; 
                     margin: 30px auto; padding: 30px; border-radius: 5px; }
        .green { font-weight: bold; color: green; }
        .message { margin-bottom: 10px; }
        label {width:70px; display:inline-block;}
        input { display:inline-block; margin-right: 10px; }
        form {line-height: 160%; }
        .hide { display: none; }
        .error { color: red; font-size: 0.9em; font-weight: bold; }
    </style>
</head>
<body>

<div id="container">

    <c:if test="${not empty message}"><div class="message green">${message}</div></c:if>

    <form:form action="valid" modelAttribute="user">
        <label for="nameInput">Name: </label>
        <form:input path="name" id="nameInput" />
        <form:errors path="name" cssClass="error" />
        <br/>

        <label for="phoneInput">Phone: </label>
        <form:input path="phone" id="phoneInput" />
        <form:errors path="phone" cssClass="error" />
        <br/>

        <label for="dateInput">Birthday: </label>
        <form:input path="date" id="dateInput" placeholder="MM/DD/YYYY" />
        <form:errors path="date" cssClass="error" />
        <br/>

        <br/>
        <input type="submit" value="Submit" />
    </form:form>
</div>

</body>
</html>

Шаг 11

Теперь создаем контроллер который и обьеденит все это в рабочее приложение, назовем его HelloController.java:

package com.devcolibri.mvc.controller;

import com.devcolibri.mvc.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.validation.Valid;

@Controller
@RequestMapping(value="/valid")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String loadFormPage(Model m) {
        m.addAttribute("user", new User());
        return "hello";
    }

    @RequestMapping(method=RequestMethod.POST)
    public String submitForm(@Valid User user, BindingResult result, Model m) {
        if(result.hasErrors()) {
            return "hello";
        }

        m.addAttribute("message", "Successfully saved User!");
        return "hello";
    }

}

После этого можно собирать Maven-ом и деплоить на любой сервер приложений.

Чтобы проверить зайдите по ссылке http://localhost:8080/[имя проекта]/valid или если деплоили на Tomcat, то http://localhost:8080/valid

Шаг 12

В результате вы увидите следующие.

1) Если данные указанно верно:

2664_3

2) Если данные указанно не верно:

2664_4

Спасибо за внимание!

Будут вопросы, то пишите в комментариях.

Урок создан: 09 августа 2013 | Просмотров: 9955 | Автор: Александр Барчук | Правила перепечатки


Добавить комментарий

Добавить комментарий

Ваш e-mail не будет опубликован.

Комментарии:

  • 17 сентября 2014 в 22:18

    Анекдот

    Привет! у меня при запуске, выводит The requested resource is not available. =(( Где может крыться эта ошибка?

  • 30 января 2015 в 17:22

    Bakhtiyor

    А как создать Constraint для проверки уникальности данных.

  • 13 ноября 2015 в 11:26

    Kolya

    Почему после попытки развернуть приложение на томкэт, томкэт пишет:
    FAIL — Deploy Upload Failed, Exception: org.apache.tomcat.util.http.fileupload.FileUploadBase$IOFileUploadException:Processing of multipart/form-data request failed. C:\Program Files\Apache Software Foundation\Tomcat 8.0\work\Catalina\localhost\manager\upload_65034154_4b4d_4d75_ab25_a541ea717411_00000005.tmp (Отказано в доступе)

  • 13 ноября 2015 в 12:10

    Kolya

    При запуске этого приложения на сервере томкэт, Ошибка: HTTP Status 500 — Servlet.init() for servlet mvc-dispatcher threw exception

  • 22 мая 2016 в 15:56

    Дима

    Проект при деплое кидает ошибку
    org.springframework.web.servlet.FrameworkServlet.initServletBean Context initialization failed
    java.lang.IllegalArgumentException
    Если возможно поправьте, а то учиться по нерабочему проекту…