Annotation-Based Validation with the Spring Bean Validation Framework

Annotation-Based Validation with the Spring Bean Validation Framework

Annotation-Based Validation with the Spring Bean Validation Framework

by Willie Wheeler
2008?7?17? ???
Use Java annotations to validate your Spring WebMVC form beans.

The Spring Bean Validation Framework, which is part of the Spring Modules project, allows you to perform validation declaratively using Java annotations. I've always liked the declarative approach, which we saw for instance in Commons Validator, but annotation-based validation is especially convenient.

JSR 303 (Bean Validation) specifies some standards around bean validation, though the Spring Bean Validation Framework does not adopt those standards. The Hibernate Validator project, on the other hand, aims to provide an implementation of the emerging JSR 303 standard.

While it very well could be subpar Googling skills on my part, there doesn't seem to be much detailed how-to information out there on actually using the Bean Validation Framework. Hence this article.

I'm using Spring 2.5.x (specifically, Spring 2.5.5) and Spring Modules 0.9. I assume that you already know Spring and Spring WebMVC in particular.

If you want to download the code, you can do so here:

contact-example.zip

You'll have to download the dependencies separately though.

Dependencies

Here's what you'll need (again, I'm using Spring 2.5.x and Spring Modules 0.9):

  • commons-collections.jar
  • commons-lang.jar
  • commons-logging.jar
  • spring.jar
  • spring-modules-validation.jar
  • spring-webmvc.jar

Java Sources

I'm going to do things a little differently than I normally do, and start with the Java first. We're going to build a very simple "Contact Us" form of the sort that you might use to ask a question, complain about lousy service, or whatever. Since we're just showing how validation works, I've left out the service and persistence tiers. We're going to do everything with a form bean and a controller.

Here's the form bean:

Code listing: contact.UserMessage
view plain copy to clipboard print ?
  1. package contact;  
  2.   
  3. import org.springmodules.validation.bean.conf.loader.annotation.handler.Email;  
  4. import org.springmodules.validation.bean.conf.loader.annotation.handler.Length;  
  5. import org.springmodules.validation.bean.conf.loader.annotation.handler.NotBlank;  
  6.   
  7. public final class UserMessage {  
  8.       
  9.     @NotBlank  
  10.     @Length(max = 80)  
  11.     private String name;  
  12.       
  13.     @NotBlank  
  14.     @Email  
  15.     @Length(max = 80)  
  16.     private String email;  
  17.       
  18.     @NotBlank  
  19.     @Length(max = 4000)  
  20.     private String text;  
  21.       
  22.     public String getName() {  
  23.         return name;  
  24.     }  
  25.   
  26.     public void setName(String name) {  
  27.         this.name = name;  
  28.     }  
  29.       
  30.     public String getEmail() {  
  31.         return email;  
  32.     }  
  33.   
  34.     public void setEmail(String email) {  
  35.         this.email = email;  
  36.     }  
  37.   
  38.     public String getText() {  
  39.         return text;  
  40.     }  
  41.   
  42.     public void setText(String text) {  
  43.         this.text = text;  
  44.     }  
  45. }  
package contact;
import org.springmodules.validation.bean.conf.loader.annotation.handler.Email;
import org.springmodules.validation.bean.conf.loader.annotation.handler.Length;
import org.springmodules.validation.bean.conf.loader.annotation.handler.NotBlank;
public final class UserMessage {
@NotBlank
@Length(max = 80)
private String name;
@NotBlank
@Email
@Length(max = 80)
private String email;
@NotBlank
@Length(max = 4000)
private String text;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

The bean itself is pretty uninteresting—I have field for the user's name, e-mail address, and the message text. But the cool part is that I've included annotations that specify validation constraints. It's probably self-explanatory, but I've specified that none of the fields is allowed to be blank, and I've also specified the maximum lengths for each. (You can also specify minimum lengths, which one could use instead of @NotBlank, but I'm using @NotBlank instead for a reason I'll explain in just a bit.) Finally, I've specified that email needs to be a valid e-mail address. It's that simple!

Here are the rest of the validation rules you can use.

Now here's the Spring MVC controller, which I've implemented as a POJO controller:

Code listing: contact.ContactController
view plain copy to clipboard print ?
  1. package contact;  
  2.   
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import org.springframework.stereotype.Controller;  
  5. import org.springframework.ui.ModelMap;  
  6. import org.springframework.validation.BindingResult;  
  7. import org.springframework.validation.Validator;  
  8. import org.springframework.web.bind.annotation.ModelAttribute;  
  9. import org.springframework.web.bind.annotation.RequestMapping;  
  10. import org.springframework.web.bind.annotation.RequestMethod;  
  11.   
  12. @Controller  
  13. public final class ContactController {  
  14.       
  15.     @Autowired  
  16.     private Validator validator;  
  17.       
  18.     public void setValidator(Validator validator) {  
  19.         this.validator = validator;  
  20.     }  
  21.       
  22.     @RequestMapping(value = "/form", method = RequestMethod.GET)  
  23.     public ModelMap get() {  
  24.           
  25.         // Because we're not specifying a logical view name, the  
  26.         // DispatcherServlet's DefaultRequestToViewNameTranslator kicks in.  
  27.         return new ModelMap("userMessage", new UserMessage());  
  28.     }  
  29.       
  30.     @RequestMapping(value = "/form", method = RequestMethod.POST)  
  31.     public String post(@ModelAttribute("userMessage") UserMessage userMsg,  
  32.             BindingResult result) {  
  33.           
  34.         validator.validate(userMsg, result);  
  35.         if (result.hasErrors()) { return "form"; }  
  36.           
  37.         // Use the redirect-after-post pattern to reduce double-submits.  
  38.         return "redirect:thanks";  
  39.     }  
  40.       
  41.     @RequestMapping("/thanks")  
  42.     public void thanks() {  
  43.     }  
  44. }  
package contact;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public final class ContactController {
@Autowired
private Validator validator;
public void setValidator(Validator validator) {
this.validator = validator;
}
@RequestMapping(value = "/form", method = RequestMethod.GET)
public ModelMap get() {
// Because we're not specifying a logical view name, the
// DispatcherServlet's DefaultRequestToViewNameTranslator kicks in.
return new ModelMap("userMessage", new UserMessage());
}
@RequestMapping(value = "/form", method = RequestMethod.POST)
public String post(@ModelAttribute("userMessage") UserMessage userMsg,
BindingResult result) {
validator.validate(userMsg, result);
if (result.hasErrors()) { return "form"; }
// Use the redirect-after-post pattern to reduce double-submits.
return "redirect:thanks";
}
@RequestMapping("/thanks")
public void thanks() {
}
}

The Bean Validation Framework includes its own Validator implementation, called BeanValidator, and I'm making that injectable here. Also, note that we're going to autowire it in.

It may be that there's a standard, predefined interceptor to apply BeanValidator (as opposed to injecting the Validator into the controller), but if there is, I haven't seen it. I'd be interested to hear if you, gentle reader, know of one.

The noteworthy method here is the second post() method, which contains the validation code. I just call the standard validate() method, passing in the form bean and the BindingResult, and return the current logical view name if there's an error. That way the form shows the validation error messages, which we'll see below. If everything passes validation, I just redirect to a "thank you" page.

Now let's look at how we define the validation messages that the end user sees if his form submission fails validation.

Validation Messages

Code listing: /WEB-INF/classes/errors.properties
view plain copy to clipboard print ?
  1. UserMessage.name[not.blank]=Please enter your name.  
  2. UserMessage.name[length]=Please enter no more than {2} characters.  
  3. UserMessage.email[not.blank]=Please enter your e-mail address.  
  4. UserMessage.email[email]=Please enter a valid e-mail address.  
  5. UserMessage.email[length]=Please enter no more than {2} characters.  
  6. UserMessage.text[not.blank]=Please enter a message.  
  7. UserMessage.text[length]=Please enter no more than {2} characters.  
UserMessage.name[not.blank]=Please enter your name.
UserMessage.name[length]=Please enter no more than {2} characters.
UserMessage.email[not.blank]=Please enter your e-mail address.
UserMessage.email[email]=Please enter a valid e-mail address.
UserMessage.email[length]=Please enter no more than {2} characters.
UserMessage.text[not.blank]=Please enter a message.
UserMessage.text[length]=Please enter no more than {2} characters.

The keys should be fairly self-explanatory given UserMessage above. Each key involves a class, a field and an annotation. The values are parametrizable messages, not unlike Commons Validator messages if you're familiar with those. In the three length messages, I'm using {2} to indicate argument #2—viz., max—for the length validation rule. Argument #1 happens to be min, and argument #0 in general appears to be the form bean itself. I can imagine that it would be nice to be able to use the form bean to get at the specific submitted value so you could say things like "You entered 4012 characters, but the limit is 4000 characters." And I think there's actually a way to do that though I don't myself know how to do it yet. (This is another one of those areas where I'd appreciate whatever information you may have.)

I mentioned above that I chose @NotBlank instead of @Length(min = 1, max = 80). The reason is that I wanted to use a specific error message ("Please enter your name") if the message is blank. I could have just used "Please enter a name between 1-80 characters" but that sounds slightly silly compared to "Please enter your name", and since I'm a usability guy I care about such things.

The JSPs

We have two JSPs: the form itself, and a basic (really basic) "thank you" page.

Code listing: /WEB-INF/form.jsp
view plain copy to clipboard print ?
  1. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>  
  2.   
  3. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  
  4.     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">  
  5.   
  6. <html xmlns="http://www.w3.org/1999/xhtml">  
  7.     <head>  
  8.         <title>Contact Us</title>  
  9.         <style>  
  10.             .form-item { margin: 20px 0; }  
  11.             .form-label { font-weight: bold; }  
  12.             .form-error-field { background-color: #FFC; }  
  13.             .form-error-message { font-weight: bold; color: #900; }  
  14.         </style>  
  15.     </head>  
  16.     <body>  
  17.       
  18. <h1>Contact Us</h1>  
  19.   
  20. <%-- Give command object a meaningful name instead of using default name, 'command' --%>  
  21. <form:form commandName="userMessage">  
  22.     <div class="form-item">  
  23.         <div class="form-label">Your name:</div>  
  24.         <form:input path="name" size="40" cssErrorClass="form-error-field"/>  
  25.         <div class="form-error-message"><form:errors path="name"/></div>  
  26.     </div>  
  27.     <div class="form-item">  
  28.         <div class="form-label">Your e-mail address:</div>  
  29.         <form:input path="email" size="40" cssErrorClass="form-error-field"/>  
  30.         <div class="form-error-message"><form:errors path="email"/></div>  
  31.     </div>  
  32.     <div class="form-item">  
  33.         <div class="form-label">Your message:</div>  
  34.         <form:textarea path="text" rows="12" cols="60" cssErrorClass="form-error-field"/>  
  35.         <div class="form-error-message"><form:errors path="text"/></div>  
  36.     </div>  
  37.     <div class="form-item">  
  38.         <input type="submit" value="Submit" />  
  39.     </div>  
  40. </form:form>  
  41.   
  42.     </body>  
  43. </html>  
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Contact Us</title>
<style>
.form-item { margin: 20px 0; }
.form-label { font-weight: bold; }
.form-error-field { background-color: #FFC; }
.form-error-message { font-weight: bold; color: #900; }
</style>
</head>
<body>
<h1>Contact Us</h1>
<%-- Give command object a meaningful name instead of using default name, 'command' --%>
<form:form commandName="userMessage">
<div class="form-item">
<div class="form-label">Your name:</div>
<form:input path="name" size="40" cssErrorClass="form-error-field"/>
<div class="form-error-message"><form:errors path="name"/></div>
</div>
<div class="form-item">
<div class="form-label">Your e-mail address:</div>
<form:input path="email" size="40" cssErrorClass="form-error-field"/>
<div class="form-error-message"><form:errors path="email"/></div>
</div>
<div class="form-item">
<div class="form-label">Your message:</div>
<form:textarea path="text" rows="12" cols="60" cssErrorClass="form-error-field"/>
<div class="form-error-message"><form:errors path="text"/></div>
</div>
<div class="form-item">
<input type="submit" value="Submit" />
</div>
</form:form>
</body>
</html>

This is just a standard Spring WebMVC form, so I'll invoke my "I assume you know Spring WebMVC" assumption here. The cssErrorClass attribute is kind of fun if you don't already know about it. It indicates the CSS class to use in the event of a validation error. You can combine that with the cssClass attribute (which applies in the non-error case) though I haven't done that here.

Now here's the basic "thank you" page:

Code listing: /WEB-INF/thanks.jsp
view plain copy to clipboard print ?
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  
  2.     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">  
  3.   
  4. <html xmlns="http://www.w3.org/1999/xhtml">  
  5.     <head>  
  6.         <title>Thank You</title>  
  7.     </head>  
  8.     <body>  
  9.         <h1>Thank You</h1>  
  10.     </body>  
  11. </html>  
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Thank You</title>
</head>
<body>
<h1>Thank You</h1>
</body>
</html>

(I told you it was basic...)

OK, now we're ready to move onto application configuration. Almost done!

Servlet and Spring Configuration

Here's our completely standard web.xml:

Code listing: /WEB-INF/web.xml
view plain copy to clipboard print ?
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <web-app xmlns="http://java.sun.com/xml/ns/javaee"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.     xsi:schemaLocation="http://java.sun.com/xml/ns/javaee  
  5.         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"  
  6.     version="2.5">  
  7.       
  8.     <servlet>  
  9.         <servlet-name>contact</servlet-name>  
  10.         <servlet-class>  
  11.             org.springframework.web.servlet.DispatcherServlet  
  12.         </servlet-class>  
  13.     </servlet>  
  14.       
  15.     <servlet-mapping>  
  16.         <servlet-name>contact</servlet-name>  
  17.         <url-pattern>/contact/*</url-pattern>  
  18.     </servlet-mapping>          
  19. </web-app>  
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<servlet>
<servlet-name>contact</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>contact</servlet-name>
<url-pattern>/contact/*</url-pattern>
</servlet-mapping>
</web-app>

Though I said I'm assuming you already know Spring WebMVC, I'll just point out that since I didn't specify a custom location for the application context file, I have to put it at /WEB-INF/contact-servlet.xml. If you want the file to live elsewhere, or if you want it to be associated with the servlet context instead of the DispatcherServlet, you'll have to set that up in web.xml accordingly.

Here's the Spring application context:

Code listing: /WEB-INF/contact-servlet.xml
view plain copy to clipboard print ?
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:context="http://www.springframework.org/schema/context"  
  4.     xmlns:p="http://www.springframework.org/schema/p"  
  5.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  6.     xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7.         http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
  8.         http://www.springframework.org/schema/context  
  9.         http://www.springframework.org/schema/context/spring-context-2.5.xsd">  
  10.       
  11.     <!-- Enable annotation-based validation using Bean Validation Framework -->  
  12.     <!-- Using these instead of vld namespace to prevent Eclipse from complaining -->  
  13.     <bean id="configurationLoader"  
  14.         class="org.springmodules.validation.bean.conf.loader.annotation  
  15. .AnnotationBeanValidationConfigurationLoader"/>  
  16.     <bean id="validator" class="org.springmodules.validation.bean.BeanValidator"  
  17.         p:configurationLoader-ref="configurationLoader"/>  
  18.       
  19.     <!-- Load messages -->  
  20.     <bean id="messageSource"  
  21.         class="org.springframework.context.support.ResourceBundleMessageSource"  
  22.         p:basenames="errors"/>  
  23.   
  24.     <!-- Discover POJO @Components -->  
  25.     <!-- These automatically register an AutowiredAnnotationBeanPostProcessor -->  
  26.     <context:component-scan base-package="contact"/>  
  27.       
  28.     <!-- Map logical view names to physical views -->  
  29.     <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"  
  30.         p:prefix="/WEB-INF/"  
  31.         p:suffix=".jsp"/>  
  32. </beans>  
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!-- Enable annotation-based validation using Bean Validation Framework -->
<!-- Using these instead of vld namespace to prevent Eclipse from complaining -->
<bean id="configurationLoader"
class="org.springmodules.validation.bean.conf.loader.annotation
.AnnotationBeanValidationConfigurationLoader"/>
<bean id="validator" class="org.springmodules.validation.bean.BeanValidator"
p:configurationLoader-ref="configurationLoader"/>
<!-- Load messages -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource"
p:basenames="errors"/>
<!-- Discover POJO @Components -->
<!-- These automatically register an AutowiredAnnotationBeanPostProcessor -->
<context:component-scan base-package="contact"/>
<!-- Map logical view names to physical views -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/"
p:suffix=".jsp"/>
</beans>

(IMPORTANT: In the configurationLoader definition, make sure you put the class name on a single line. I had to break it up for formatting purposes.)

If you're not familiar with it, I'm using the p namespace here for syntactic sugar—it allows me to specify properties using a nice shorthand.

The first two beans basically create the BeanValidator instance we're going to use. It turns out that instead of defining these two beans explicitly, you can use a special element from a special namespace:

  • namespace is xmlns:vld="http://www.springmodules.org/validation/bean/validator";
  • purported schema location is http://www.springmodules.org/validation/bean/validator-2.0.xsd;
  • element is <vld:annotation-based-validator id="validator"/>

But when I do it, Eclipse complains (even though the code works when you run it) since there isn't at the time of this writing actually an XSD at the specified location. (At least there's a JIRA ticket for it.) So I'll just use the two beans for now.

The other stuff is mostly normal Spring WebMVC stuff. I put the message source on the app context, scan for the controller (which is why I'm autowiring the validator into the controller), and put a view resolver on the context too.

Finis

Build and deploy your WAR, and then point your browser to your web app; for example:

http://localhost:8080/contact-example/contact/form

Try submitting the form with empty fields, or an invalid e-mail address, or fields that are too long. If things are working correctly, you ought to see error messages and even field highlighting when validation fails.

And that, my friends, is it! Feel free to post a comment if you run into problems getting it to work and I'll try to help if I can.

Again, if you want to download the sample code (minus dependencies; see above), here it is:

contact-example.zip

Have fun!

你可能感兴趣的:(Annotation-Based Validation with the Spring Bean Validation Framework)