这是注释处理的好模式吗?

时间:2011-07-21 00:40:43

标签: java design-patterns annotations

我有一个相当标准的Spring webapp,我有一些自定义注释,我想用它来表示应用于给定web服务方法的要求和约束。例如,我可以将@RequiresLogin注释应用于需要有效用户会话的任何方法,并对需要设置“name”和“email”的方法应用@RequiresParameters(paramNames = {"name", "email"}),依此类推。

为了支持这一点,我实现了一个ad-hoc实用程序,用于在运行时验证方法的注释约束,基本上遵循以下模式:

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'
}
//...

这很好用,但由于我添加了额外的注释,因此变得有点笨拙。我想用更易于维护的东西替换它。理想情况下,我希望能够做到:

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

但我很确定这是不可能的,因为注释本身不能包含可执行代码,因为编译器不会让我扩展java.lang.annotation.Annotation(不是我知道如何允许可执行代码包含在注释中,即使编译器让我试试)。

然而,注释可以包含嵌套的内部类,并且内部类可以执行普通Java类可以执行的任何操作。所以我基于这个想法,并且为了保证我的验证代码与尽可能验证的注释密切相关,我是:

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

然后注释可以像:

一样实现
@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;
        }
    }
}

这使得验证逻辑保持良好并与正在验证的注释紧密耦合。然后我的所有临时验证代码都可以替换为:

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;
}

每次添加新的注释类型时,都不会再需要修改特殊代码。我只是将验证器作为注释的一部分来实现,我已经完成了。

这看起来像一个合理的模式吗?如果不是那么什么可能更好?

3 个答案:

答案 0 :(得分:2)

您可能想要调查AOP。您可以建议公开某些注释的方法,并相应地执行前/后处理。

答案 1 :(得分:1)

我想补充一点,虽然AOP是一个很好的解决方案,但Spring框架已经通过@Secured注释提供了这个功能。

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

}

Spring还支持使用@Valid注释进行JSR-303验证。所以至少对于这些用例,似乎你正在重新发明轮子。

答案 2 :(得分:0)

恕我直言可以考虑将访客模式与工厂结合起来。工厂将返回一个包装器对象,该对象知道确切的注释类型以及访问者将能够...

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);
        }
    }
}

由于我们无法扩展Annotation,因此我们需要工厂中的那些包装类。您还可以传递原始注释,然后将其包含在该包装类中。

你需要做的事情:为每个新的AnnotationType添加一个新的“包装”类到工厂,扩展工厂的

getVisitable()

方法相应地并且还向访问者添加相应的方法:

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

现在通用验证(或其他)代码如下:

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

访问通过间接访问工作,访问对象将自己作为参数调用访问者,因此将调用正确的访问方法。 希望有所帮助;-) 代码未经过测试,但是......