3

I've got a fairly standard Spring webapp, and I have a number of custom annotations that I would like to use to denote the requirements and constraints applied to a given web-service method. For instance, I might apply an @RequiresLogin annotation to any method that requires a valid user session, and @RequiresParameters(paramNames = {"name", "email"}) on a method that requires that "name" and "email" be set, and so on.

In support of this I implemented an ad-hoc utility for validating a method's annotated constraints at runtime, which basically followed a pattern of:

Map<Class<? extends Annotation>, Annotation> annotations = mergeConstraintsFromClassAndMethod(serviceClass, serviceMethod);
if (annotations.containsKey(AnnotationType1.class)) {
    AnnotationType1 annotation = (AnnotationType1)annotations.get(AnnotationType1.class);
    //do validation appropriate to 'AnnotationType1'
}
if (annotations.containsKey(AnnotationType2.class)) {
    AnnotationType2 annotation = (AnnotationType2)annotations.get(AnnotationType2.class);
    //do validation appropriate to 'AnnotationType2'
}
//...

This works fine, but has become a bit unwieldy as I have added additional annotations. I'd like to replace it with something a bit more maintainable. Ideally I'd like to be able to do:

List<ValidatableAnnotation> annotations = mergeConstraintsFromClassAndMethod(serviceClass, serviceMethod);
for (ValidatableAnnotation annotation : annotations) {
    annotation.validate(request);
}

But I'm pretty sure that is not possible since annotations themselves cannot contain executable code and since the compiler will not let me extend java.lang.annotation.Annotation (not that I'd know how to go about allowing executable code to be contained in an annotation even if the compiler let me try).

What annotations can contain, however, is a nested inner class, and that inner class can do anything that a normal Java class can do. So what I've come up with based upon that and in the interest of keeping my validation code as closely associated with the annotation being validated as possible is:

public interface AnnotationProcessor {
    public boolean processRequest(Annotation theAnnotation, HttpServletRequest request);
}

And then the annotations can be implemented like:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequiresLogin {

    public static class Processor implements AnnotationProcessor {

        @Override
        public boolean processRequest(Annotation theAnnotation, HttpServletRequest request) {
            if (! (theAnnotation instanceof RequiresLogin)) {
                //someone made an invalid call, just return true
                return true;
            }
            return request.getSession().getAttribute(Constants.SESSION_USER_KEY) != null;
        }
    }
}

Which keeps the validation logic nice and tightly coupled with the annotation that is being validated. Then all my ad-hoc validation code can be replaced with:

List<Annotation> annotations = mergeConstraintsFromClassAndMethod(serviceClass, serviceMethod);
for (Annotation annotation : annotations) {
    processAnnotation(annotation, request);
}


private static boolean processAnnotation(Annotation annotation, HttpServletRequest request) {
    AnnotationProcessor processor = null;
    for (Class<?> processorClass : annotation.annotationType().getDeclaredClasses()) {
        if (AnnotationProcessor.class.isAssignableFrom(processorClass)) {
            try {
                processor = (AnnotationProcessor)processorClass.newInstance();
                break;
            }
            catch (Exception ignored) {
                //couldn't create it, but maybe there is another inner 
                //class that also implements the required interface that 
                //we can construct, so keep going
            }
        }
    }
    if (processor != null) {
        return processor.processRequest(annotation, request);
    }

    //couldn't get a a processor and thus can't process the 
    //annotation, perhaps this annotation does not support
    //validation, return true
    return true;
}

Which leaves no more ad-hoc code that needs to be revised every time I add a new annotation type. I just implement the validator as part of the annotation, and I'm done.

Does this seem like a reasonable pattern to use? If not then what might work better?

aroth
  • 54,026
  • 20
  • 135
  • 176
  • 2
    +1 for teaching me you can put an inner class inside an annotation, that's interesting to know. I agree with hoipolloi that AOP is a good solution, it will also save you from having to put a call to your validation method at the top of every annotated method. – OpenSauce Jul 21 '11 at 08:51
  • 1
    @OpenSauce - There's no call to the validation method at the start of every annotated method. That would kind of defeat the purpose of doing it this way. The validation is performed as part of the top-level Spring controller (`DispatcherServlet`) which serves as the gateway to the annotated service API. There's one call in there to run the pre-request validation prior to allowing the request to proceed to the service API, and another call to run post-request validation prior to rendering the `ModelAndView` returned by the service API, and that's it. – aroth Jul 21 '11 at 13:14

3 Answers3

2

You may want to investigate AOP. You can advise methods that expose certain annotations and perform pre/post processing accordingly.

hoipolloi
  • 7,984
  • 2
  • 27
  • 28
  • AOP is an option, since what I built basically behaves as an AOP framework. But will a third-party library correctly handle things like "inheritance" of annotations? With what I have right now I can annotate a class with something like `@RequiresLogin`, and every method in that class will behave as if annotated with `@RequiresLogin` unless they explicitly override the "inherited" annotation(s). (sorry for the double comment, I noticed a couple of obnoxious typos that I felt compelled to correct) – aroth Jul 21 '11 at 13:17
  • @aroth: In a word, yes. You can define a pointcut for all methods in a class with a specific annotation. See this question for more info: http://stackoverflow.com/questions/2011089/aspectj-pointcut-for-all-methods-of-a-class-with-specific-annotation – hoipolloi Jul 21 '11 at 18:51
  • Right, but if I want all methods in a class annotated with a specific annotation *except* methods annotated with some other annotation, will it handle that? Without me needing to enumerate every possible permutation? For instance, I annotate `ClassA` with `@RequiresLogin` and then on `method1` and `method2` I have no annotations, and on `method3` I have `@NoInheritance`, I want to match `method1` and `method2`, but not `method3`. Although the [AspectJ syntax](http://download.oracle.com/javaee/6/api/javax/validation/ConstraintValidator.html) seems a bit more obtuse than I'd prefer. – aroth Jul 22 '11 at 05:59
  • @aroth: Depends on your AOP implementation. AspectJ could easily handle that. Suggest you research AspectJ's pointcut expression language. – hoipolloi Jul 22 '11 at 07:09
  • @aroth: "Although the AspectJ syntax seems a bit more obtuse than I'd prefer." - the syntax can certainly seem obscure to the uninitiated but it seems to almost be the AOP standard now; even if you're not directly using AspectJ. For example, I understand Spring has adopted the AspectJ pointcut expression language for it's proxy-based AOP implementation. – hoipolloi Jul 22 '11 at 07:12
1

I would just like to add that while AOP would be a good solution, the Spring framework already provides this functionality by way of the @Secured annotation.

@Secured("ROLE_USER")
public void foo() {

}

Spring also supports JSR-303 validation with the @Valid annotation. So for these use cases at least, it seems you are re-inventing the wheel.

hoipolloi
  • 7,984
  • 2
  • 27
  • 28
  • Not quite reinventing the wheel, as the webapp is not using a role-based authorization model. JSR-303 looks interesting, but I didn't see any obvious way of getting the `HttpServletRequest` into a [ConstraintValidator](http://download.oracle.com/javaee/6/api/javax/validation/ConstraintValidator.html) implementation (which is required in essentially every case). – aroth Jul 22 '11 at 06:00
  • @aroth: Yes, I (incorrectly) assumed that since you were using Spring, you would also be using Spring MVC. – hoipolloi Jul 22 '11 at 07:07
0

IMHO one could think about the Visitor pattern in combination with a factory. The factory will return a wrapper object that knows the exact annotation type and which the visitor will be able...

class MyVisitor {
    public void visit(VisitableAnnotationType1 at) {
        //something AnnotationType1 specific
    }
    public void visit(VisitableAnnotationType2 at) {
        //something AnnotationType2 specific
    }
    ... // put methods for further annotation types here
}

class VisitableFactory {
    public abstract class VisitableAnnotation {
        public abstract void accept(MyVisitor visitor);
    }

    class VisitableAnnotationType1 implements VisitableAnnotation {
        public void accept(MyVisitor visitor) {
            visitor.visit(this);
        }
    }

    public static VisitableAnnotation getVisitable(Annotation a) {
        if(AnnotationType1.class.isAssignableFrom(a.getClass()) {
            //explicitely cast to the respective AnnotationType
            return new VisitableAnnotationType1((AnnotationType1)a);
        } else if (AnnotationType2.class.isAssignableFrom(a.getClass()) {
            //explicitely cast to the respective AnnotationType
            return new VisitableAnnotationType1((AnnotationType1)a);
        }
    }
}

As we cannot extend Annotation, we need those wrapper classes in the factory. You could also pass the original annotation which is then contained in that wrapper class.

What you have to do: For each new AnnotationType add a new "wrapper" class to the factory, extend the factory's

getVisitable()

method accordingly and also add an according method to the Visitor:

public void doSomething(VisitableAnnotationTypeXYZ at) {
    //something AnnotationTypeXYZ specific
}

now the generic validation (or whatever) code looks like:

List<ValidatableAnnotation> annotations = mergeConstraintsFromClassAndMethod(serviceClass, serviceMethod);
MyVisitor visitor = new MyVisitor();
for (ValidatableAnnotation annotation : annotations) {
    VisitableFactory.getVisitable(annotation).accept(visitor);
}

The visiting works by the indirection that the visited object calls the visitor with itself as the argument and thus the correct visit method will be invoked. Hope that helps ;-) Code is not tested, though...