0

I want to decorate existing objects so that method calls are automatically validated. I already managed to delegate method call to an interceptor that calls Hibernate validator and so far it works fine:

public class HibernateBeanValidator implements BeanValidator{

    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();

    @Override
    public <T> T addMethodValidation(T object) {
        ExecutableValidator executableValidator = factory.getValidator().forExecutables();

        Class<? extends T> dynamicType = (Class<? extends T>)new ByteBuddy()
                .subclass(object.getClass())
                .method(isPublic()).intercept(MethodDelegation.to(new ValidationInterceptor(object, executableValidator)).andThen(SuperMethodCall.INSTANCE))
                .make()
                .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();

        try {
            T validatedObject = dynamicType.newInstance();
            return  validatedObject;
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static class ValidationInterceptor {

        private final Object validatedObject;
        private final ExecutableValidator executableValidator;

        public <T> ValidationInterceptor(T object, ExecutableValidator executableValidator) {
            this.validatedObject = object;
            this.executableValidator = executableValidator;
        }

        public void validate(@Origin Method method, @AllArguments Object[] arguments)
                throws Exception {
            Set<ConstraintViolation<Object>> constraintViolations = executableValidator.validateParameters(validatedObject, method, arguments);
            if(! constraintViolations.isEmpty()) {
                throw new ValidationException(constraintViolations);
            }
        }
    }
}

What I would like to improve is to bind method calls only to methods that have at least one parameter annotated with a constraint annotation, such as:

class Echo {
    public String repeat(@NotNull String word) { /* should bind validation here */
        return word;
    }

    public String notAnnotated(String word) { /* should not bind validation */
        return word;
    }
}

How could I specify an ElementMatcher in Byte Buddy so that it would bind only to methods with parameters annotated with annotations that are annotated with @Constraint, such as @NotNull (taken from javax.validation.constraints):

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {

    String message() default "{javax.validation.constraints.NotNull.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    /**
     * Defines several {@link NotNull} annotations on the same element.
     *
     * @see javax.validation.constraints.NotNull
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
    @Retention(RUNTIME)
    @Documented
    @interface List {

        NotNull[] value();
    }
}
Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
starko
  • 11
  • 4

2 Answers2

0

Your problem can be solved by implementing a custom ElementMatcher which is used to identify methods to be intercepted. Currently, you are using the predefined isPublic() interceptor which does not consider annotations but only the public modifier of a method. As the predefined annotations can be chained, you can build a suitable matcher as follows:

isPublic().and(hasParameter(hasAnnotation(nameStartsWith("javax."))))

Of course, you can simply implement your own matchers without using the predfined ones.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
0

Actually instead of just checking for an annotation out of the javax.validation.constraints namespace, it is probably better to use the Bean Validation meta data API. Constraints do not need to come from this namespace, but can also originate from Hibernate Validator (org.hibernate.validator.constraints) or be a custom constraint. A possible implementation of ElementMatcher which makes use of the meta data API could look like this:

public static class BeanValidationMatcher implements ElementMatcher {

private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

@Override
public boolean matches(Object target) {
    // handle different descriptors and potentially use generic MethodDescription
    if ( !( target instanceof MethodDescription.ForLoadedMethod ) ) {
        return false;
    }
    MethodDescription.ForLoadedMethod methodDescription = (MethodDescription.ForLoadedMethod) target;
    Method method = methodDescription.getLoadedMethod();

    boolean isGetter = ReflectionHelper.isGetterMethod( method );

    boolean needsValidation;
    BeanDescriptor beanDescriptor = validator.getConstraintsForClass( method.getDeclaringClass() );
    if ( isGetter ) {
        needsValidation = isGetterConstrained( method, beanDescriptor );
    }
    else {
        needsValidation = isNonGetterConstrained( method, beanDescriptor );
    }

    return needsValidation;
}

private boolean isNonGetterConstrained(Method method, BeanDescriptor beanDescriptor) {
    return beanDescriptor.getConstraintsForMethod( method.getName(), method.getParameterTypes() ) != null;
}

private boolean isGetterConstrained(Method method, BeanDescriptor beanDescriptor) {
    String propertyName = ReflectionHelper.getPropertyName( method );
    PropertyDescriptor propertyDescriptor = beanDescriptor.getConstraintsForProperty( propertyName );
    return propertyDescriptor != null && propertyDescriptor.findConstraints()
            .declaredOn( ElementType.METHOD )
            .hasConstraints();
}

}

Hardy
  • 18,659
  • 3
  • 49
  • 65