【问题标题】:Is this a good pattern for annotation processing?这是注释处理的好模式吗?
【发布时间】:2011-07-21 00:40:43
【问题描述】:

我有一个相当标准的 Spring webapp,并且我有许多自定义注释,我想用它们来表示应用于给定 web 服务方法的要求和约束。例如,我可以将@RequiresLogin 注释应用于任何需要有效用户会话的方法,@RequiresParameters(paramNames = {"name", "email"}) 应用于需要设置“名称”和“电子邮件”的方法,等等。

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

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

这样就不再需要在每次添加新注释类型时修改临时代码了。我只是将验证器实现为注释的一部分,就完成了。

这看起来是一种合理的使用模式吗?如果没有,那么什么会更好?

【问题讨论】:

  • +1 教我你可以在注释中放置一个内部类,这很有趣。我同意 hoipolloi 的观点,即 AOP 是一个很好的解决方案,它还可以让您不必在每个带注释的方法的顶部调用验证方法。
  • @OpenSauce - 在每个带注释的方法开始时都不会调用验证方法。这有点违背了这样做的目的。验证作为顶级 Spring 控制器 (DispatcherServlet) 的一部分执行,该控制器充当带注释的服务 API 的网关。在允许请求继续到服务 API 之前,其中有一个调用来运行请求前验证,另一个调用是在呈现服务 API 返回的 ModelAndView 之前运行请求后验证,仅此而已。

标签: java design-patterns annotations


【解决方案1】:

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

【讨论】:

  • AOP 是一个选项,因为我构建的基本上表现为 AOP 框架。但是第三方库会正确处理注释的“继承”之类的事情吗?使用我现在拥有的东西,我可以用@RequiresLogin 之类的东西来注释一个类,并且该类中的每个方法都会表现得好像用@RequiresLogin 注释一样,除非它们显式地覆盖“继承的”注释。 (抱歉重复评论,我注意到一些令人讨厌的错别字,我觉得有必要更正)
  • @aroth:总之,是的。您可以使用特定注释为类中的所有方法定义切入点。有关更多信息,请参阅此问题:stackoverflow.com/questions/2011089/…
  • 是的,但是如果我希望类中的所有方法都使用特定注释进行注释 except 方法使用其他注释进行注释,它会处理吗?不需要我列举所有可能的排列?例如,我用@RequiresLogin注释ClassA,然后在method1method2上我没有注释,在method3上我有@NoInheritance,我想匹配method1method2,但不是method3。虽然AspectJ syntax 似乎比我想要的更钝。
  • @aroth:取决于你的 AOP 实现。 AspectJ 可以轻松处理。建议你研究 AspectJ 的切入点表达语言。
  • @aroth:“虽然 AspectJ 的语法似乎比我喜欢的要迟钝一些。” - 语法对于外行来说当然看起来晦涩难懂,但现在它似乎几乎是 AOP 标准;即使您没有直接使用 AspectJ。例如,我了解 Spring 采用了 AspectJ 切入点表达式语言来实现基于代理的 AOP。
【解决方案2】:

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

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

}

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

【讨论】:

  • 没有完全重新发明轮子,因为 webapp 没有使用基于角色的授权模型。 JSR-303 看起来很有趣,但我没有看到任何将HttpServletRequest 引入ConstraintValidator 实现的明显方法(基本上每种情况都需要)。
  • @aroth:是的,我(错误地)假设你使用的是 Spring,你也会使用 Spring MVC。
【解决方案3】:

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

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

访问是通过被访问对象以自身为参数调用访问者的间接方式进行的,从而调用正确的访问方法。 希望有帮助;-) 不过,代码没有经过测试...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-11-03
    • 1970-01-01
    • 2015-03-23
    • 2011-01-29
    • 2011-01-01
    • 2012-02-19
    • 2015-03-30
    • 2012-01-13
    相关资源
    最近更新 更多