Custom bean validator with Spring framework and API/implementation barrier

I feel a big urge to decrease the amount of boilerplate code in the applications I am working on. Writing this code is not productive, not just because of the code itself, but also because of the testing for that code. The bean validation framework can help reduce such boilerplate code.
I created the following constraint to help in validation the role on our REST services:

/**
 * Annotation to automatically validate a security role for validity and possibly check the role.
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidTokenDelegate.class)
public @interface ValidToken {
 
    /**
     * Boodschap als de validatie fout.
     */
    String message() default "Token not valid.";
 
    /**
     * Groups for the constraint.
     */
    Class<?>[] groups() default { };
 
    /**
     * Payload.
     */
    Class<? extends Payload>[] payload() default { };
 
    /**
     * Required roles, one of them is sufficient.
     */
    String[] vereisteRollen() default { };
}

Note that while the documentation does not seem to indicate this, including the message, groups and payload fields in the annotation is mandatory. Luckily hibernate-validator gives clear indication about this.

The actual code which does the validation is quite easy. The actual validation itself is done using an injected spring service.

@Component
@Scope("prototype")
public class ValidTokenValidator implements ConstraintValidator<ValidToken, TokenTO> {
 
    private String[] vereisteRollen;
 
    @Autowired
    private RequestStateService requestStateService;
 
    @Override
    public void initialize(ValidToken constraintAnnotation) {
        vereisteRollen = constraintAnnotation.vereisteRollen();
    }
 
    @Override
    public boolean isValid(TokenTO token, ConstraintValidatorContext context) {
        try {
            requestStateService.validate(token, vereisteRollen);
            return true;
        } catch (AccessDeniedException se) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(se.getMessage()).
                    addConstraintViolation();
            return false;
        } catch (ServiceException se) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(se.getMessage()).
                    addConstraintViolation();
            return false;
        }
    }
}

Unfortunately, there are two problems to get this to work. The application is modular with a clear split between API – containing only interfaces and transfer objects – and implementation modules. The service actually checking the constraint only exists in the implementation module, but the constraint needs to be specified on interfaces in the API module. This means the ValidToken annotation also needs to be in the API module (correctly so). The @Constraint annotation makes the annotation a bean validation constraint, but it requires a reference to the class name, which breaks the module split.
Secondly we have a problem with the spring injection in the validator class. The system is wired together automatically, simply by including the dependency to hibernate-validator in the maven pom. It then requires just a few lines in the spring configuration:

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

Unfortunately though, it seems both Hibernate-validator and Spring try to define the validator factory, and I found no way to assure the spring one is defined last (though I basically need both anyway).

As a solution to both problems, I introduced a delegate for the validator.

@Component // @todo component not needed, used to force autowiring as LocalValidatorFactoryBean is not used
public class ValidTokenDelegate implements ConstraintValidator<ValidToken, TokenTO> {
 
    private static final Logger LOG = LoggerFactory.getLogger(ValidTokenDelegate.class);
 
    private static ApplicationContext applicationContext;
 
    private ConstraintValidator constraintValidator;
 
 
    /**
     * Assure application context is set.
     * @todo dirty, automatic injection of autowired stuff is not working
     *
     * @param applicationContext application context
     */
    @Autowired
    public void setApplicationContext(ApplicationContext applicationContext) {
        LOG.info("ValidTokenValidatorDelegate is wired.");
        ValidTokenDelegate.applicationContext = applicationContext;
    }
 
    @Override
    public void initialize(ValidToken constraintAnnotation) {
        constraintValidator = (ConstraintValidator) applicationContext.getBean("validTokenValidator");
        constraintValidator.initialize(constraintAnnotation);
    }
 
    @Override
    public boolean isValid(TokenTO value, ConstraintValidatorContext context) {
        return constraintValidator.isValid(value, context);
    }
}

The delegate is placed in the API module. While this is not conceptually the right place, it fixes the dependency cycle.
The class is annotated using @Component to assure that spring wiring takes place. The wiring is done using a setter which sets a static field. This makes the field available in all instances. This is likely to cause problems if you try to change you application context without restarting the application.
Note that the application context is autowired and not the validTokenValidator bean. This is mandatory. The bean validation framework allows caching validator instances, calling the initialize method only once and isValid more often (possibly from different threads). This requires a new ValidTokenValidator for each instance, so the bean needs to be a prototype scope and you have to assure a new bean is created for each delegate instance.

Edit Nov 2015: there seems to be another way, see http://www.twitlonger.com/show/n_1snrbeo

Leave a Reply

Your email address will not be published. Required fields are marked *

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*