`
teamojiao
  • 浏览: 344036 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

Spring Validation with Java Annotations

阅读更多

Spring Framework provides a ready-to-use, simple validation framework out of the box. In this article, I'll explore how to use Java annotations to make Spring's validation framework even more convenient to use, while providing a basic introduction to Spring validation and Java annotations in general.

Introduction

Spring's data binding and validation framework is simple but functional - all you need to do is implement the Validator interface. In particular, if you use Spring's MVC framework (and its command/form controllers) then you're probably using Spring validation as well. If not, you should be! Moving your validation outside the controller into a Validator implementation allows you to declaratively assign and reuse your validation logic.

But while you can declaratively assign and reuse Validators with your form or command controllers, doing so only allows you to declare validation on a 'per-class' level, with no field-level granularity.

A typical scenario

For example, consider the typical scenario where you have a LoginForm class to represent your typical login form with two fields username and password. It's trivial to write a LoginFormValidator that checks that both fields are present and assign that to the controller that handles login.

What if you now need a RegistrationForm controller, with additional fields such as verifyPassword and emailAddress? Well, you could extend LoginFormValidator or simply add an new Validator to the registration controller for the additional required fields. All is still good and dandy.

But what about other form or command objects? What if, a PasswordChangeForm needs only the password and verifyPassword fields? How about a UserSearchForm with only the username?

You could of course write unique Validators for individual fields, but that's a terrible CodeSmell. At this point you know you need to write a more generic validation implementation with field-level granularity.

But, depending upon your approach, you might end up not being able to use Spring's declarative approach to assigning validators to controllers. Or, you might have to implement your own mechanism to allow declarative validation rules using fancy XML. Or use YetAnotherThirdPartyLibrary, which, for a small application or during its initial stages may be overkill and cause unnecessary code bloat.

Annotations to the rescue

Fortunately, Java 5.0 annotations give us a convenient way to mark object elements or fields in a way that lets us roll out an equally simple and extensible, yet more granular validation implementation based on Spring.

The rest of this article details a simplistic approach to this, using the common, if rather trivial example above. At the end of the article I have some thoughts on how one can take this approach and make it more functional and useful.

Our typical LoginForm

package foo.bar;

public class LoginForm {

    private String username;

    private String password;

    public String getUsername() {
        return this.username;
    };

    // snip...
}

Nothing exciting happening here, I'll just put it here for reference.

Annotating classes

First we'll want to 'mark' our class so that our annotation-aware validators can easily recognize them later (without having to inspect down to the class members). Let's create our first annotation:

package foo.bar;

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validatable {

}

As you can see, there's really nothing there. It's a plain marker annotation. A few things about the annotations that annotate our annotation are in order, though.

The "@Target(ElementType.TYPE)" annotation tells the compiler that Validatable should only be applied to Types or classes.

"@Retention(RetentionPolicy.RUNTIME)" indicates that this annotation should be recorded into the class file and retained by the VM at run-time, enabling it to be introspected. This is probably the most important part of this whole annotation business for our purposes, as it's crucial to how our validator will work later.

We can now use @Validatable to annotate our classes so they can be identified and validated by our validation framework later. Next, we'll create the @Required annotation to indicate required fields.

Annotating fields

package foo.bar;

import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Required {

}

Note that while I said @Required will be used to annotate required fields, I indicate that it's target should be a METHOD. In particular, we'll be using @Required to annotate field or property 'getters'.

There are two reasons for this. One is I'm a lazy bum who likes to get things done the simplest way possible. Next is that if we use @Required on a class' private fields then we'd have to do double work to introspect the class just to find our annotation(s) (as opposed to being able to use Spring's BeanUtils to help us out). So instead, I opt to use @Required on the getField() methods.

Annotations in action

Armed with these two annotations, let's go ahead and apply them to our LoginForm.

@Validatable
public class LoginForm {

    // ...snip

    @Required
    public String getUsername() {
        return this.username;
    };

    // ...snip...

    @Required
    public String getUsername() {
        return this.username;
    };

    // snip...

}

As you can see, writing annotations is quite painless, especially simple marker annotations with no parameters (or elements) like ours.

Also, using annotations in one's code doesn't require much work, either (just remember to import them if they're in a different package). In fact, annotations can lend to better code clarity - after all, that's what they're there for in the first place.

A Spring Validator in 3 easy steps

Now let's make this magic happen. We'll write a RequiredAnnotationValidator that uses introspection to find our annotations, and perform validation on the required fields.

Step 1: Implement Validator

public class RequiredAnnotationValidator implements Validator {

// snip...

Because we're implementing Spring's Validator interface, we need to implement its two primary methods supports() and validate().

Step 2: Implement supports()

What classes should our RequiredAnnotationValidator support? Why, classes annotated as @Validatable, of course. How do we find out if a class is annotated as @Validatable using introspection? Why, it's almost trivial:

    @SuppressWarnings("unchecked")
    public final boolean supports(final Class clazz) {
        return clazz.isAnnotationPresent(Validatable.class);
    }

As you can see if we had only used our @Required annotation, our validator would have to do a lot more work - go through the methods of the given Class clazz sequentially, looking for any methods annotated as @Required and returning true once it finds one. Annotating the class itself is simpler and faster.

A little note about the @SuppressWarnings above. If you're using Eclipse IDE, the line "clazz.isAnnotationPresent(...)" will generate a warning because we're calling a method on the raw type Class (and not a parameterized version of the generic type Class < T >). It's kind of a nuisance - though we can get rid of the warning with a fugly cast: "Class<?> c = clazz" - so we tell Eclipse to just shut up.

Step 3: Implement validate()

Now to the meat of the matter, implementing validate(). Now that you've seen how supports() works, you should have a general idea of how validate() is supposed to work.

First we'll want a list of getter methods. We could use standard Java Reflection, or, since we're already using Spring:

    public final void validate(final Object obj, final Errors errors) {
        // list the object's properties
        final PropertyDescriptor[] propertyDescriptors 
            = BeanUtils.getPropertyDescriptors(obj.getClass());

...

We can rely on BeanUtils.getPropertyDescriptors() for convenience since, by our own convention, we're looking for annotations in property getter methods. As I mentioned earlier, had we annotated private fields, we'd have to use Class.getDeclaredFields(), iterate through the fields looking for corresponding accessor methods, etc. Our approach is a lot less painful.

What do we do with each property descriptor? First check if the property has a read method, and if that method is annotated as @Required (since we're using Java 5.0, we'll go ahead and use an enhanced for loop):

        for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {

            // see if the property (getter) is annotated as required
            final Method method = propertyDescriptor.getReadMethod();
            if (method != null && method.isAnnotationPresent(Required.class)) {

At this point we know that the field foo is required because the method getFoo() was annotated with @Required. Now all we have to do is check that its value is not null, or, since we want to be a little smart, check that it's not the empty string either (""). Fortunately, once again Spring provides utility code in ValidationUtils to do just the job:

                final String field = propertyDescriptor.getName();
                ValidationUtils.rejectIfEmpty(errors, field, field + ".required");

Here we just told Spring to check if the given field is empty, and if it is, reject it with the error code "fieldname.required" and place that error into the passed in errors object.

The complete validator

Here's a more-or-less complete look at our RequiredAnnotationValidator (minus any import statements):

package foo.bar;

public class RequiredAnnotationValidator implements Validator {

    @SuppressWarnings("unchecked")
    public final boolean supports(final Class clazz) {
        return clazz.isAnnotationPresent(Validatable.class);
    }

    public final void validate(final Object obj, final Errors errors) {

        // list the object's properties
        final PropertyDescriptor[] propertyDescriptors 
            = BeanUtils.getPropertyDescriptors(obj.getClass());

        for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {

            // see if the property (getter) is annotated as required
            final Method method = propertyDescriptor.getReadMethod();
            if (method != null && method.isAnnotationPresent(Required.class)) {

                // reject with "field.required" if null/empty
                final String field = propertyDescriptor.getName();
                ValidationUtils.rejectIfEmpty(errors, field, field + ".required");

            }
        }
    }

}

Voila!

Annotation-based validation based on Spring's validation framework in a couple of dozen lines of code, how's that? Here's how we'd use it in an sample Spring application context XML descriptor file:

  <bean name="requiredAnnotationValidator" class="foo.bar.RequiredAnnotationValidator"/>

  <!-- e.g., foo.bar.LoginController extends SimpleFormController -->
  <bean name="loginController" class="foo.bar.LoginController">
    <property name="commandClass"><value>foo.bar.LoginForm</value></property>
    <property name="validator"><ref bean="requiredAnnotationValidator"/></property>
  </bean>

If we need to use more than one validator for LoginController, then we simply use the plural validators property of BaseCommandController.

Conclusion

Simple, extensible and granular declarative validation is possible using the Spring framework, aided by Java annotations.

There are a lot of more things we can do from here. We could use an annotation element as a parameter to declaratively specify the error code for our required field (as opposed to a hard-coded "field.required"). We can go ahead and create other annotations, for example, @MaxLength with a length parameter.

One drawback to our approach is the fact that the validation has to perform introspection every time it's asked to validate an object. For form command object binding and validation, this isn't such a problem. However, this can lead to performance issues especially if the validation is called repeatedly on a large collection of objects.

One enhancement I could think of is to have the validators cache the introspection results so they don't have to do it every time (and do so in a thread-safe manner).

This article has outlined a gentle introduction Spring validation as well as Java annotations. I hope now you know how easy they both can be, and that you're now as excited as I am on their added posssiblities.

Alistair Israel started learning programming with BASIC on a TRS-80. That was a long time ago. He's still learning.

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics