【问题标题】:Compile time validation of method arguments方法参数的编译时验证
【发布时间】:2017-03-07 09:10:02
【问题描述】:

我在这里发现了一些类似的问题,但不完整的答案没有帮助,而且比澄清任何事情都产生了更多的困惑,所以我尝试提出一个更有条理的问题,并希望得到有助于更多用户的答案。

我的简化示例:我有一个带有两个不同构造函数的 Java 类

public class ObjectOfInterest {
  public ObjectOfInterest(String string, Integer int) { ... }
  public ObjectOfInterest(String string1, String string2) { ... }
  ...
}

我需要对这些构造函数的调用进行一些编译时验证。参数string2 必须是一些文字,我想根据内容将调用标记为警告(即,当它不是文字或文字格式不正确时发出警告)。

不幸的是,使用 Eclipse 在 Java 中进行验证的文档不容易理解,有时已经过时,在我看来通常不完整,而且似乎没有足够短的工作示例可以在教程中使用。

我的目标: 第一步:我想要一个验证器,用警告标记这两个参数版本的调用 - 只是为了从某个地方开始并了解基础知识。

到目前为止我发现了什么: 我见过的几个例子是public class MyValidator implements IValidator, ISourceValidator,其中IValidator 需要实现一个方法public void validate(IValidationContext arg0, IReporter arg1) throws ValidationException,并且似乎来自旧版本的验证框架(有时我发现只是带有注释的空方法useless),而ISourceValidator 需要实现一个方法public void validate(IRegion arg0, IValidationContext arg1, IReporter arg2) - 这似乎是最新的版本。

然后你必须为一些plugin.xml添加一些扩展点(我不完全清楚这个plugin.xml在哪里)。

我在黑暗中刺伤的地方:完全不清楚如何使用 IRegionIValidationContextIReporter - 也许我走错了路,但该怎么办我到这里?如何在验证中找到该构造函数的调用?

在第一步变得更加清晰之后,我会扩展这个问题。 Outlook,我想为构造函数的两个 String 版本添加快速修复的可能性,并以这种方式操作代码 - 但这至少提前两步,稍后会详细介绍。

【问题讨论】:

  • 仅作记录:如果您有两个不相关的构造函数,那么您的类很可能违反了单一职责原则。
  • 您的目标是改进代码,还是编写 Eclipse 插件?因为“编译时验证”基本上是 Java 类型系统,即类。如果您想对参数进行约束,请为它们定义一个适当的类,而不是添加一些 IDE 支持。
  • 我认为这不是以这种方式解决它的好方法。具体来说,检查字符串是否是一些文字的验证是没有用的,因为如果它应该是一组特定的文字,那么它应该是一个枚举(并且不需要额外的编译时检查),如果它应该是有一些特定的格式,那么很可能这种格式应该被提取到它自己的类中并正确记录(同样,除了 javac 中已经存在的内容之外,不需要编译时检查)。
  • 好吧,我不知道它有多简单,但你可以试试Unnecessary Code Detector
  • @biziclop - 它有点复杂,我必须检查某个类中的成员是否存在(以及它是否有同名的 getter) - 当然我可以优化以某种方式编写代码,让它变得更好,生成常量等等——关键是,这将花费太长时间。提供所需的最便宜的东西是在编译时进行一些检查 - 或者像 FindBugs 或 SonarQube 这样的检查也可以 - 我不确定什么更容易实现。

标签: java eclipse validation


【解决方案1】:

首先我不得不说,您尝试超越了普通的 Java 编程。在编译时没有正常的验证方法,除了可以使用普通类型实现的验证。

您还想做的事情超出了使用注释处理器可以做的事情。注释处理器是一种半正常的,因为它们是标准化的并且是 Java 框架的一部分。它们是在编译期间运行的类,它们以类和方法的签名作为输入,可用于验证和代码生成。

如果你还是这样还有不正常的办法,但是:


Eclipse 插件解决方案

您似乎正在尝试的解决方案是编写一个使用 Eclipse Java 工具进行验证的 Eclipse 插件。应该可以,不知道会不会容易,而且验证只对使用 Eclipse 的用户有效。


检查器解决方案

根据我(有限的)知识,最好的工具是:

Checker Framework 用于静态分析。

它完全用于您想做的事情。它似乎有据可查且易于设置。例如,它用于对正则表达式语法进行空值分析和编译时验证。有一个tutorial 听起来和你的东西很相似。在creating a new checker的手册中有一章。

使用它来解决可能需要花费大量时间和精力,但我也认为它看起来很有趣!

【讨论】:

    【解决方案2】:

    注意:

    这可能不是确切的解决方案。但我想讨论 解决该问题的可能方法。我已经提到了 可能有助于工作的替代方法。请加入 讨论。

    我想尝试在JDK-5 中引入并在JDK-6JSR-269 规范下标准化的Java 注释处理器

    需要注释文件 (java @interfaces),这些文件应该使用自定义注释根据规则进行验证。如果无法注释每个文件,则必须注释包含要验证的类的包(也可以遍历内部包)。以下示例将演示如何使用带注释的类和注释处理器来验证类。我已将示例项目上传到 github 存储库中。请查看github上的项目仓库https://github.com/hjchanna/compile_validation_java

    步骤 01:创建注释接口 (@interface)

    package com.mac.compile_validation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author hjchanna
     */
    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.TYPE)
    public @interface CompileValidation {
    
    }
    

    STEP 02:创建Processor类,向编译器引入编译验证规则

    处理器类应该extend 来自javax.annotation.processing.AbstractProcessor,并且应该使用@SupportedAnnotationTypes@SupportedSourceVersion 注释进行注释。请根据具体要求或验证规则修改CompileValidationProcessor类。

    package com.mac.compile_validation;
    
    import java.util.List;
    import java.util.Set;
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.annotation.processing.SupportedAnnotationTypes;
    import javax.annotation.processing.SupportedSourceVersion;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.ExecutableElement;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.element.VariableElement;
    import javax.lang.model.type.TypeMirror;
    import javax.lang.model.util.ElementFilter;
    import javax.tools.Diagnostic;
    
    /**
     *
     * @author hjchanna
     */
    @SupportedAnnotationTypes("com.mac.compile_validation.CompileValidation")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class CompileValidationProcessor extends AbstractProcessor {
    
        /**
         * Processes a set of annotation types on type elements originating from the
         * prior round and returns whether or not these annotation types are claimed
         * by this processor. If {@code
         * true} is returned, the annotation types are claimed and subsequent
         * processors will not be asked to process them; if {@code false} is
         * returned, the annotation types are unclaimed and subsequent processors
         * may be asked to process them. A processor may always return the same
         * boolean value or may vary the result based on chosen criteria.
         *
         * The input set will be empty if the processor supports {@code
         * "*"} and the root elements have no annotations. A {@code
         * Processor} must gracefully handle an empty set of annotations.
         *
         * @param annotations the annotation types requested to be processed
         * @param roundEnv environment for information about the current and prior
         * round
         * @return whether or not the set of annotation types are claimed by this
         * processor
         */
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            //Iterate through compiling files which annotated with @CompileValidation
            for (Element elem : roundEnv.getElementsAnnotatedWith(CompileValidation.class)) {
                //find type element for element
                TypeElement typeElement = findEnclosingTypeElement(elem);
    
                //required parameter types
                TypeElement stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String");
                TypeElement integerType = processingEnv.getElementUtils().getTypeElement("java.lang.Integer");
    
                //find construtors according to your scenario
                ExecutableElement conA = findConstructor(typeElement, stringType.asType(), integerType.asType());
                ExecutableElement conB = findConstructor(typeElement, stringType.asType(), stringType.asType());
    
                //check availability of constructors, if not available it should show a warning message in compile time
                if (conA == null || conB == null) {
                    String message = "Type " + typeElement + " has malformed constructors.";
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message);
                }
    
            }
            return true; // no further processing of this annotation type
        }
    
        /**
         * Returns a constructor which have two parameters and parameter types equal
         * to paramA and paramB. Return null if required constructor is not
         * available.
         *
         * @param typeElement like the class which may constructors encapsulated
         * @param paramA first parameter of required constructor
         * @param paramB second parameter of required constructor
         * @return constructor which have required parameters
         */
        private static ExecutableElement findConstructor(TypeElement typeElement, TypeMirror paramA, TypeMirror paramB) {
            List<ExecutableElement> executableElements = ElementFilter.constructorsIn(typeElement.getEnclosedElements());
    
            for (ExecutableElement executableElement : executableElements) {
                List<VariableElement> variableElements = (List<VariableElement>) executableElement.getParameters();
    
                //match constructor params and length
                if (variableElements.size() == 2
                        && variableElements.get(0).asType().equals(paramA)
                        && variableElements.get(1).asType().equals(paramB)) {
                    return executableElement;
                }
            }
    
            return null;
        }
    
        /**
         * Returns the TypeElement of element e.
         *
         * @param e Element which contain TypeElement
         * @return Type element
         */
        public static TypeElement findEnclosingTypeElement(Element e) {
            while (e != null && !(e instanceof TypeElement)) {
                e = e.getEnclosingElement();
            }
    
            return TypeElement.class.cast(e);
        }
    }
    

    步骤03:创建处理服务链接文件

    然后需要在项目的资源路径(/src/main/resources/META-INF/services)中添加一个名为javax.annotation.processing.Processor的类。该文件仅包含Processor 的类名。按照上例配置文件内容如下。

    com.mac.compile_validation.CompileValidationProcessor
    

    之前的方法适用于maven项目。如果需要,可以将配置文件手动注入到输出.jar 文件的META-INF/services 文件夹中。

    步骤 04:禁用当前项目的验证

    禁用当前项目的注释处理。如果启用,它将无法构建当前项目,因为编译器会尝试定位 Processor 类进行验证。但它仍然没有编译。所以它会因为自己而无法构建项目。将以下代码添加到pom.xml(在&lt;build&gt; -&gt; &lt;plugin&gt; 内)。

    <compilerArgument>-proc:none</compilerArgument>
    

    现在差不多完成了。唯一需要做的就是将构建输出 .jar 文件依赖添加到原始项目中。

    是时候测试项目了。使用之前创建的自定义注释来注释所需的类 (CompileValidation)。如果无法验证带注释的类,它将显示警告。我的输出如下。

    替代解决方案

    • 可以使用 PMD,这是一个 java 源代码扫描器。它提供了使用 xml 配置定义规则的方法。
    • 尝试在启动时使用 java 反射验证类。 (这不是你问的问题。但在开始像 spring、hibernate 和其他知名框架一样工作之前验证内容是一个好习惯。)
    • 尝试Java Instrumentation API,但如果应用程序违反规则,唯一可能的反应就是使应用程序崩溃。这不是一个好习惯。

    【讨论】:

    • 如果我没记错的话,这个解决方案会验证构造函数的声明的格式,而不是构造函数的调用。验证电话是这个问题的意义所在。据我所知,没有办法使用注释处理器检查方法主体,所以我认为使用这种方法不可能解决问题。
    猜你喜欢
    • 1970-01-01
    • 2012-10-06
    • 1970-01-01
    • 1970-01-01
    • 2012-01-13
    • 1970-01-01
    • 1970-01-01
    • 2021-11-25
    • 1970-01-01
    相关资源
    最近更新 更多