Tuesday 25 May 2010

Creating a custom JSR 303 constraint annotation with Spring 3

Spring 3.0 has support for JSR303 annotation-driven validation, which makes it easy to build declarative validation into Java Beans in Spring applications. Out of the box, JSR303 supports annotations such as @NotNull and @Size etc which allow you to perform basic validation checks. However, most applications will need to do more sophisticated "business-logic" validation such as checking whether an email already exists in a database. I recently needed to do just that in a Spring MVC application that needed a "Forgot Password" function to let users receive an email with their password. The Spring MVC command object was very simple:
public class SendPasswordReminderCommand implements Serializable {
 
 private static final long serialVersionUID = 42L;

 private String email;

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }

      // equals, hashcode, toString, constructors omitted
}
Annotating this class with JSR 303 annotations to check the format of the email or ensure that the email is not null is easy enough (using @NotNull, @Length from the core javax.validation.constraints standard package and @Email from the org.hibernate.validator.constraints package). Ideally, you would also be able to check the database and ensure that the email exists in the database and inform the user if not as part of the validation. To do this, having a custom annotation called @EmailExistsConstraint would be ideal - but you need to make your own!
public class SendPasswordReminderCommand implements Serializable {
 
 private static final long serialVersionUID = 42L;
 
 @NotNull
 @Length(min=1)
 @Email
 @EmailExistsConstraint
 private String email;

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }
}
Creating the custom constraint requires you to create two classes - an annotation interface and associated constraint validator. The annotation is straightforward.

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

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy=EmailExistsConstraintValidator.class)

public @interface EmailExistsConstraint {
 String message() default "Email doesn't exists";
 Class[] groups() default {};
 Class[] payload() default {};
}

The constraint validator is where you perform the database check. As this is a Spring and Hibernate application, the constraint validator uses a Hibernate DAO as a "collaborator" to perform the check.
public class EmailExistsConstraintValidator implements ConstraintValidator {

 private Log log = LogFactory.getLog(EmailExistsConstraintValidator.class);
 
 @Autowired
 private EmployeeFinder employeeFinder;
 
 @Override
 public void initialize(EmailExistsConstraint constraint) {
  
 }

 @Override
 public boolean isValid(Object target, ConstraintValidatorContext context) {
  
  try {
   Collection employees = employeeFinder.findEmployeeByEmail((String) target);
   if (employees.size() > 0) {
    return true;
   }
  } catch (Exception e) {
   log.error(e);
  }
  return false;
 }

 public void setEmployeeFinder(EmployeeFinder employeeFinder) {
  this.employeeFinder = employeeFinder;
 }
}

This is a fairly straightforward constraint validator implementation. If the database contains a record matching the email address, the isValid() method returns true and false if not. All that Spring needs to use JSR303 is to register the Spring validator implementation in the application context. It then automatically detects any custom constraints and will validate classes using your annotations without any further configuration.

However, there is still one problem with this. As it stands, the Validator will validate all of the annotations in one pass. This is a problem, because the database check will occur even if the email fails all of the other checks which is wasteful and potentially confusing to the user, who would see multiple validation messages. Thankfully, the JSR303 specification contains the concept of groups, which can be used to lump annotations together and process them in a specified order. The example below illustrates how groups can be used to split the basic format checks from the database check.
@GroupSequence(value={SendPasswordReminderCommand.class, FormatChecks.class,BusinessLogicChecks.class})
public class SendPasswordReminderCommand implements Serializable {
 
 private static final long serialVersionUID = 42L;
 
 @NotNull(groups=FormatChecks.class)
 @Length(min=1, groups=FormatChecks.class)
 @Email(groups=FormatChecks.class)
 @EmailExistsConstraint(groups=BusinessLogicChecks.class)
 private String email;

 public String getEmail() {
  return email;
 }

 public void setEmail(String email) {
  this.email = email;
 }
}
Firstly, each annotation has a group parameter that specifies a class (in this example, FormatChecks.class and BusinessLogicChecks.class). These classes are marker interfaces that are simply used to identify groups. To specify the sequence in which annotation groups are processed, the @GroupSequence annotation is used. An array of classes is passed to the values parameter that starts by convention with the class being validated, followed by the group marker interfaces in the order that you wish to process them. The marker interfaces are very straightforward. One of the interfaces must extend the javax.validation.groups.Default marker interface, which as you may guess is the default group if no group is specified.
public interface FormatChecks extends Default {

}

public interface BusinessLogicChecks {

}
You can define as many groups as you want. As you can see, creating custom constraint annotations is pretty straightforward with Spring 3.0. JSR 303 is well thought out and the ability to group constraints is very useful for ensuring that expensive business logic checks are not performed without reason, when validation has already failed for basic data format reasons.

17 comments:

  1. This is interesting. There's been some discussion around additional support for specifying groups during validation for Spring. I have a slightly different issue where a command object spans multiple pages for data entry. I need a way to specify when to validate (ex only on page 2). Unfortunately, @Valid doesn't allow (yet) the ability to specify for what groups. Have you run into this issue?

    Thanks,

    Mark

    ReplyDelete
  2. Sorry for the late reply. You could do this with your own custom @Valid annotation in the Controller. It's not that hard to do and is well explained in this post: http://blog.newsplore.com/2010/02/23/spring-mvc-3-0-rest-rebuttal (scroll down the page). I was also thinking of writing about how I created my own custom @Valid attribute for validating service methods declaratively. HTH Pat

    ReplyDelete
  3. Hi,

    thanks for the post..

    With this way, we have to write a new interface and class for each business logic validation right? is there a way we can reduce this work? (Something generic)

    also,
    could you please post an example here regarding custom @valid tag.

    ReplyDelete
  4. I think it would be difficult to write a generic ConstraintValidator as the collaborators and indeed business logic rules would differ in each case. Have a look at http://nonrepeatable.blogspot.com/2010/05/validating-service-method-parameters.html for my implementation of a custom AOP annotation, which was inspired by http://blog.newsplore.com/2010/02/23/spring-mvc-3-0-rest-rebuttal.

    One way you *could* implement a generic business rules validation would be to use a rules engine like Drools. That would still require you to create your validation logic using Drools, but the validator would simply invoke Drools on the object being validated.

    ReplyDelete
  5. Hi! nice post!

    Just a question, are there some constraints packages available for download?

    ReplyDelete
  6. Not sure what you mean. The reference implementation of JSR303 (Hibernate Validator) has a number of annotations for download. Plus, quite a few other bloggers have put example constraints online. I was trying to show that it's easy to create your own!

    ReplyDelete
  7. Thanks much PAT. But what I am looking is.. is there a way where we can validate only some portions of the model object at one time? Eg: (In Controller)@Valid01 should validate properties 1-5 of the model and @Valid02 should validate properties 5-10 of the model object etc.

    Normally, @Valid would validate all the properties of the model object right. That I dont want.. I want to be able to customize the validation.. yet use annotations and proceed (pls without using aspects and AOP)

    ReplyDelete
  8. That's what the groups are for. You would use @Valid in conjunction with other annotations like @NotNull in which you specify the groups as described above.

    Note that the @Valid annotation itself doesn't support groups. This has been raised as an issue for Spring, but it really relates to the JSR303 spec itself.

    ReplyDelete
  9. true Pat.. but the groups only specify the order.. what I am looking for is I want to be able to want/don't want to do some validations (of bean fields).

    using groups and @Valid validate All the fields in some order or other? right?

    ReplyDelete
  10. You can also use groups as a parameter to the validate() method in the Validator interface. The validation will only occur for the groups you specify. Again, this is only through programmatic use of the validator and I'm not sure this can be done declaratively with Spring.

    http://download.oracle.com/javaee/6/api/javax/validation/Validator.html

    ReplyDelete
  11. This comment has been removed by the author.

    ReplyDelete
  12. @sai krishna
    I faced kinda same problem, days of headache and I came out with this solution:
    in main appcontext.xml
    < mvc:annotation-driven validator="YourValidator" />
    < bean id="YourValidator" class="com.your.package.YourValidator" />
    YourValidator class:
    @Component
    public class YourValidator implements org.springframework.validation.Validator
    {
    @Override
    public void validate( Object obj, Errors errors )
    {
    ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();

    javax.validation.Validator validator = validatorFactory.getValidator();
    if(yourCustomCondition)
    {
    Set> beanViolations = validator.validate( obj );
    }
    }
    }

    for(ConstraintViolation v : beanViolations)
    {
    errors.rejectValue( "path", "messageCode", "defaultMessage" );
    }


    maybe it helps
    Sergo

    ReplyDelete
  13. Hi, Thanks for this thread.

    I might have missed something, but I tried to reproduce the same behavior: injecting a business bean in the constraint validor, but this bean is not injected at all when the constraint validator is executed.
    Maybe the reason is that the validator is not constructed when the application start-up ? Did you have to do something special in the spring context file ?

    Thanks

    ReplyDelete
  14. This comment has been removed by the author.

    ReplyDelete
  15. Hi Damien - the only thing you need to do is add to your application context the following bean definition:

    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />

    The validation framework will (i.e. should) auto-detect all validator implementations and register them with the Validator.

    ReplyDelete
  16. Hi When using this, I am getting a NULL pointer exception, on googling it I found that:

    We need to either build a ConstraintValidatorFactory or use SpringConstraintValidatorFactory if you want to wire dependencies.

    http://stackoverflow.com/questions/13350537/inject-service-in-constraintvalidator-bean-validator-jsr-303-spring

    Please advise, how to do that.

    ReplyDelete
  17. CAN U PROVIDE THE FULL PROJECT

    ReplyDelete