如何使用在编译时设置的动态数量的参数创建一个java方法(类似lombok的行为)

1Da*_*co1 5 java parameters methods enums parameter-passing

我想创建一个Message枚举,每个消息都在枚举类型上,以避免错误与消息键中的拼写错误.我还想使用参数(如#{0})来插入名称和更多信息.为了使事情变得更容易,我想添加方法get,该方法具有动态数量的(字符串类型)参数 - 每个我想要替换的参数都有一个.参数的确切数量应在编译时设置,并由该枚举值的字段定义.

考虑一下这个枚举:

public enum Message {
    // Written by hand, ignore typos or other errors which make it not compile.

    NO_PERMISSION("no_permission", 0),
    YOU_DIED("you_died", 1),
    PLAYER_LEFT("player_left", 2);

    private String key;
    private int argAmount;

    Message(String key, int argAmount) {
        this.key = key;
        this.argAmount = argAmount;
    }

    public String replace(String... args) {
        String message = get();
        for (int i = 0; i < args.length; i++) {
            message.replace("#{" + i + "}", args[i]);
        }

        return message;        
    }

    public String get() {
        return myConfigFileWrapper.getMessage(key);
    }
}
Run Code Online (Sandbox Code Playgroud)

当我想要检索消息时,我会使用Message.YOU_DIED.replace(myInformation).但是,我必须查找YOU_DIED消息所占用的参数数量,如果有多个,我需要查看配置文件以查看哪个索引属于哪个参数类型.

为了澄清这一点,这里有一个例子:该PLAYER_LEFT消息被广播给所有玩家并告诉他们玩家x已经离开了得分y.在我的.lang文件中,人们会发现player_left= The player #{0} left with the score #{1}!.在源代码中,我将需要使用Message.PLAYER_LEFT.replace(name, score).当我的枚举现在扩展时,我可能有超过100条消息.这意味着我根本不记得,如果消息是The player #{0} left with the score #{1}!The player #{1} just left!.

我的目标是,当get没有给出方法所需的确切数量时,编译器会自动抛出错误.这也意味着我的IDE自动完成功能将告诉我要传递多少个参数.

如您所见,目前我正在使用varargs将可变信息注入到消息中.为什么我要进一步采取这一步骤现在应该清楚.我知道这是一种奢侈的功能,但我只是在学习,没有人希望在某个时候得到某种结果.

一种方法是Message类,其中包含大量子类,使用一组参数覆盖原始get方法:get(String name, String score).但是,这会使数十亿个子类 - 每个消息一个子类 - 搞得一团糟.我甚至没有尝试创建这种Message类.此外,使用这种方式需要花费很多精力来"创建"所有消息,然后再添加新消息.

接下来,我查看了反射API以使其工作,但是一旦我认为反射对于动态编译时方法不起作用,我就继续.据我所知,实际上创建新的动态方法(基本上是我尝试做的)是不可能的,特别是因为通过普通调用不能使用它们,因为该方法在编译时不存在.

到目前为止,我所知道的唯一应用程序是Lombok.Lombok使用注释,这些注释在编译时被字节代码替换.我查看了源代码,但只是核心本身非常大,并且在各处都有交叉依赖,这使得很难真正理解发生了什么.

使用在编译时设置的动态参数编号生成这些方法的最佳和最简单的方法是什么?那说的方式如何运作?

我们非常感谢您提供代码片段以及指向具有更多信息的页面的链接.

1Da*_*co1 0

经过很多个小时的阅读和实验,我现在终于有了自己的注释处理器和源代码生成器。

\n\n

感谢 @biziclop、@bayou.io 和 @Aasmund Eldhuset 对这个问题提供了 3 个非常不同但很好的答案,解释了智能方法。这个答案被接受,因为这是OP(我)最终使用的方法。如果您不想像我一样在项目中投入那么多工作,也可以考虑查看它们。

\n\n

我按照 @Radiodef 在他的评论中发布的指南进行操作,一切都很顺利,直到我到达他解释如何将注释处理器与 Maven 集成的地步。在开始使用 Maven 并遵循该指南时遇到了一些困难之后,事实证明,Apache Maven 过去是、现在仍然是用于此类注释处理的最佳依赖关系和构建管理工具。因此,如果您也阅读该指南并使用 Maven,我建议您跳过第 2 部分。

\n\n

但是,现在,\xe2\x80\x99 不在于发生了哪些问题,而在于必须做什么才能使其工作:\n所需的 Maven 依赖项:org.apache.velocity:velocity:1.7:jar

\n\n

项目设置发生了一些变化,因为带有源的实际项目将包含在根容器项目中。这不是必需的,但它允许更清晰的项目结构和更易读的 POM。

\n\n

有 4 个 POM:

\n\n
    \n
  • 根项目
  • \n
  • 实际项目
  • \n
  • 注释
  • \n
  • 注释处理器
  • \n
\n\n

如前所述,RootProject 不包含任何源代码,也不包含任何文件,但一般包含其他项目,因此它的 pom 很简单:

\n\n
<modules>\n    <module>ActualProject</module>\n    <module>Annotations</module>\n    <module>AnnotationProcessors</module>\n</modules>\n\n<!\xe2\x80\x94 Global dependencies can be configured here as well \xe2\x80\x94>\n
Run Code Online (Sandbox Code Playgroud)\n\n

ActualProject 显然取决于 Annotations 工件和 AnnotationProcessors 工件。由于 AnnotationProcessors 工件依赖于 Annotation 项目,因此我们得到 Maven Reactor 的以下顺序:

\n\n
    \n
  1. 注释
  2. \n
  3. 注释处理器
  4. \n
  5. 实际项目
  6. \n
\n\n

我们还需要配置哪些项目执行注释处理器,哪些不执行。注释处理器本身不应在其自身编译期间执行,因此添加编译器参数-proc:none

\n\n
<plugin>\n     <groupId>org.apache.maven.plugins</groupId>\n     <artifactId>maven-compiler-plugin</artifactId>\n     <version>3.3</version>\n     <configuration>\n         <compilerArgs>\n             <arg>-proc:none</arg>\n         </compilerArgs>\n     </configuration>\n</plugin>\n
Run Code Online (Sandbox Code Playgroud)\n\n

对于实际项目,我们也会在正常编译时以同样的方式禁用注释处理,并将maven-processor-pluginbuild-helper-maven-plugin一起使用:

\n\n
<plugin>\n    <groupId>org.bsc.maven</groupId>\n    <artifactId>maven-processor-plugin</artifactId>\n    <version>2.2.4</version>\n    <executions>\n        <!-- Run annotation processors on src/main/java sources -->\n        <execution>\n            <id>process</id>\n            <goals>\n                <goal>process</goal>\n            </goals>\n            <phase>generate-sources</phase>\n            <configuration>\n                <outputDirectory>target/generated-sources</outputDirectory>\n                <processors>\n                    <processor>my.annotations.processors.MessageListProcessor</processor>\n                </processors>\n            </configuration>\n        </execution>\n    </executions>\n</plugin>\n\n<plugin>\n    <groupId>org.codehaus.mojo</groupId>\n    <artifactId>build-helper-maven-plugin</artifactId>\n    <version>1.9.1</version>\n    <executions>\n        <execution>\n            <id>add-source</id>\n            <phase>generate-sources</phase>\n            <goals>\n                <goal>add-source</goal>\n            </goals>\n            <configuration>\n                <sources>\n                    <source>target/generated-sources</source>\n                </sources>\n            </configuration>\n        </execution>\n    </executions>\n</plugin>\n
Run Code Online (Sandbox Code Playgroud)\n\n

注释工件最重要的是包含具有 String 类型的值字段的注释以及注释类也必须实现的接口。\n枚举必须实现两个方法,显然是String getKey()String[] getParams()。接下来,问题(消息)中的枚举扩展如下:

\n\n
@MessageList("my.config.file.wrapper.type")\npublic enum Messages implements MessageInfo {\n\n    NO_PERMISSION("no_permission"),\n    YOU_DIED("you_died",                "score"),\n    PLAYER_LEFT("player_left",          "player_name", "server_name");\n\n    private String key;\n    private String[] params;\n\n    Messages(String key, String\xe2\x80\xa6 params) {\n        this.key = key;\n        this.params = params;\n\n    @Override\n    public String getKey() { return key; }\n\n    @Override\n    public String[] getParams() { return params; }\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

接下来,我们的 AnnotationProcessor。当然,我们实现AbstractProcessor并因此 @Override process 方法。该类还使用注释注册自身@SupportedAnnotationTypes("my.annotation.type")。首先,我们对带注释的类执行一些检查。请注意,带有注解的元素是在集合中传递的,这意味着会有一个 foreach 循环。然而,预计在一个项目中永远只能找到一个注释。 @MessageList这显然是一个潜在的风险,特别是当它用于非特定项目时。在这里,\xe2\x80\x99 并不重要,因为我们知道如何正确使用注释。\n(可以扩展此处理器以从多个枚举收集消息,但根本不需要\xe2\x80\x99。 )

\n\n
for (Element e : roundEnv.getElementsAnnotatedWith(MessageList.class)) {\n    if (!(e.getKind() == ElementKind.ENUM)) {\n        raiseErrorAt(e, "Can only annotate enum types");\n        continue;\n    } ... }\n
Run Code Online (Sandbox Code Playgroud)\n\n

接下来,我们必须检查带注释的类是否实际实现了该接口。只是一个小问题:带注释的类尚未编译\xe2\x80\x99。MessageInfo 接口的类对象很容易获得:

\n\n
Class<MessageInfo> messageInfoClass = (Class<MessageInfo>) Class.forName("my.annotations.MessageInfo");\n
Run Code Online (Sandbox Code Playgroud)\n\n

是的,这确实是未经检查的强制转换,但我们使用常量字符串值,因此这不会\xe2\x80\x99t 导致 ClassCastException。不管怎样,让\xe2\x80\x99s编译带注释的类。这意味着,带注释的类必须导入可能尚未编译的任何其他类。它应该\xe2\x80\x99t,因为它仅作为丰富的资源,并且从技术上讲也可以是一个.properties 文件。同样,这也是一个潜在的风险,同样,我们不关心,因为我们不导入任何其他内容。

\n\n
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();\nStandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);\n\n// The convertToPath method just returns "src/main/java/<pathWithSlashes>.java"\nIterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(\n    new File("ActualProject/" + convertToPath(element.getQualifiedName().toString())));\n\n// The boolean here defines whether the last separator char should be cut off.\n// We need to expand the class path so we might as well leave it there.\nString classpath = getCurrentClasspath(false) +\n    new File("Annotations/target/Annotations-version.jar").getAbsolutePath();\n\nFile outputDir = new File("ActualProject/target/classes/");\nIterable<String> arguments = Arrays.asList("-proc:none",\n    "-d", outputDir.getAbsolutePath(),\n    "-classpath", classpath);\n\nboolean success = compiler.getTask(null, fileManager, null, arguments, null, compilationUnits).call();\n\nfileManager.close();\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后,检查 success 的值,返回 is it\xe2\x80\x99s false。\n这里是 getCurrentClassPath 方法:

\n\n
private String getCurrentClasspath(boolean trim) {\n    StringBuilder builder = new StringBuilder();\n    for (URL url : ((URLClassLoader) Thread.currentThread().getContextClassLoader()).getURLs()) {\n        builder.append(new File(url.getPath()));\n        builder.append(System.getProperty("path.separator"));\n    }\n    String classpath = builder.toString();\n    return trim ? classpath.substring(0, classpath.length() - 1) : classpath;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,编译带注释的类后,我们可以加载它:

\n\n
URL classesURL = new URL("file://" + outputDir.getAbsolutePath() + "/");\n// The current class loader serves as the parent class loader for the custom one.\n// Obviously, it won\xe2\x80\x99t find the compiled class.\nURLClassLoader customCL = URLClassLoader.newInstance(new URL[]{classesURL}, classLoader);\n\nClass<?> annotatedClass = customCL.loadClass(element.getQualifiedName().toString());\n
Run Code Online (Sandbox Code Playgroud)\n\n

因此,这里检查带注释的枚举是否实现了该接口:

\n\n
if (!Arrays.asList(annotatedClass.getInterfaces()).contains(messageInfoClass)) {\n    raiseErrorAt(element, "Can only annotate subclasses of MessageInfo");\n    continue;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,读取传递给源代码生成器的值:

\n\n
MessageList annotation = element.getAnnotation(MessageList.class);\nString locals = annotation.value();\n\n// To get the package name, I used a while loop with an empty body. Does its job just fine.\nElement enclosingElement = element;\nwhile (!((enclosingElement = enclosingElement.getEnclosingElement()) instanceof PackageElement)) ;\nString packageName = ((PackageElement) enclosingElement).getQualifiedName().toString();\n\nArrayList<Message> messages = new ArrayList<>();\nfor (Field field : annotatedClass.getDeclaredFields()) {\n    if (!field.isEnumConstant()) continue;\n\n    // Enum constants are static:\n    Object value = field.get(null);\n    MessageInfo messageInfo = messageInfoClass.cast(value);\n\n    messages.add(new Message(field.getName(), messageInfo.getKey(), messageInfo.getParams()));\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里使用的 Message 类只是一个具有私有最终字段和相应 getter 方法的数据类。它在注释工件中找到,但我不确定将它放在哪里。\n而\xe2\x80\x99s 它!现在可以实例化速度引擎和上下文并传递值。拼图的最后一块是源的模板。\n首先,我创建了 3 个变量,但有特殊字符,因为我在将速度\xe2\x80\x99s 转义工具集成到我的项目\xe2\x80\xa6 中时非常失败

\n\n
#set ($doubleq = \'"\')\n#set ($opencb = "{")\n#set ($closecb = "}\xe2\x80\x9c)\npackage $package;\n
Run Code Online (Sandbox Code Playgroud)\n\n

类体几乎只是一个 foreach 循环:

\n\n
/**\n * This class was generated by the Annotation Processor for the project ActualProject.\n */\npublic abstract class Message {\n\n#foreach ($message in $messages)\n\n#set ($args = "")\n#set ($replaces = "")\n#foreach ($param in $message.params)\n#set ($args = "${args}String $param, ")\n#set ($replaces = "${replaces}.replace($doubleq$opencb$param$closecb$doubleq, $param)")\n#end\n#set ($endIndex = $args.length() - 2)\n#if ($endIndex < 0)\n#set ($endIndex = 0)\n#end\n#set ($args = $args.substring(0, $endIndex))\n    public static final String ${message.name}($args) {\n        return locals.getMessage("$message.key")$replaces;\n    }\n\n#end\n\n    private static final $locals locals = ${locals}.getInstance();\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

乍一看,这组庞大的 Velocity 指令可能有点奇怪,但它确实很简单。没有空行,因为实际上会生成空行,从而使生成的文件非常混乱。那么做了什么呢?我们迭代所有消息。对于每条消息执行以下操作:

\n\n
    \n
  1. 定义两个String类型的变量,args和replaces
  2. \n
  3. 对于消息采用的每个参数:\n\n
      \n
    • 将字符串 \xe2\x80\x9eString 、 \xe2\x80\x9e 附加到 args 变量。
    • \n
    • 将字符串 \xe2\x80\x9e.replace(\xe2\x80\x9e{}\xe2\x80\x9c, )\xe2\x80\x9c 附加到 params 变量。
    • \n
  4. \n
  5. 从 args 变量中删除最后一个逗号和空格。(当消息没有参数时,endIndex 为负值。如果是这种情况,请将 endIndex 设置为 0。)
  6. \n
  7. 使用枚举常量的名称以及 2 和 3 中生成的参数字符串生成实际方法。\n\n
      \n
    • 该方法返回通过处理不同语言的类检索的消息,并替换占位符。
    • \n
  8. \n
\n\n

在文件的末尾,我们定义了 Locals 类的实例。我的第一个计划是使用一个接口,但这并没有\xe2\x80\x99 效果太好,所以我只要求该类是一个单例。第三次,这是另一个潜在的风险,第三次出于同样的原因被忽视。

\n\n

哦,您可能偶然发现的 raiseErrorAt(Element, String) 方法只是 imo 很长的调用的包装器processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element);

\n\n

我希望这有帮助。完整的项目在这里公开。对于本文中引用的提交,请参阅此处。\n如果有任何问题或改进,请随时发表评论。

\n