繁体   English   中英

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

[英]How to make a java method with a dynamic number of arguments which is set at compile-time (lombok-like behavior)

我想创建一个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);
    }
}

当我想要检索消息时,我使用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使用注释,这些注释在编译时被字节代码替换。 我查看了源代码,但只是核心本身非常大,并且在各处都有交叉依赖,这使得很难真正理解正在发生的事情。

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

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

您可以通过为每个不同数量的参数创建一个通用子类来限制子类的数量:

public class Message {
    public static final Message0Args NO_PERMISSION = new Message0Args("no_permission");
    public static final Message1Arg YOU_DIED = new Message1Arg("you_died");
    public static final Message2Args PLAYER_LEFT = new Message2Args("player_left");

    private String key;
    private int argAmount;

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

    // Same replace() method, but make it protected
}

子类是例如:

public class Message2Args extends Message {
    public Message2Args(String key) {
        super(key, 2);
    }

    public String replace(String first, String second) {
        return super.replace(first, second);
    }   
}

请注意, Message不再是enum ,但出于所有实际目的,它的工作方式相同(有一些额外的灵活性,如子类化),因为enum只是一个类的语法糖,其唯一的实例包含在自己的public static final字段中。

麻烦的是,即使您知道参数的数量,您仍然不知道它们应该是什么。 Message.PLAYER_LEFT.replace(name, score)还是Message.PLAYER_LEFT.replace(score, name) 或者它可能是Message.PLAYER_LEFT.replace(name, lastLocation)

为了避免它,你可以更进一步,做这样的事情:

public abstract class Message<T> {

    public static final Message<Void> YOU_DIED = new Message<Void>("You died.") {
        @Override
        public String create(Void arguments) {
            return this.replace();
        }
    };

    public static final Message<Player> PLAYER_LEFT = new Message<Player>("Player %s left with score %d") {
        @Override
        public String create(Player arguments) {
            return this.replace( arguments.getName(), arguments.getScore());
        }
    };

    private Message(String template) {
        this.template = template;
    }

    private final String template;

    protected String replace( Object ... arguments) {
        return String.format( template, arguments );
    }

    public abstract String create(T arguments);
}

不可否认,这是相当冗长的,但有一些事情要做:

  1. 所有消息都是类型安全的。
  2. 你可以(实际上你必须)使用更高级别的对象,这有希望带来更多意义。 虽然很难弄清楚Message.PLAYER_LEFT的两个String参数应该包含哪些内容,但如果唯一的参数是Player类型的对象,那么答案就很明显了。
  3. 除此之外,如果您希望更改消息以显示播放器的昵称或级别,那该怎么办? 您需要修改的只是实际消息,呼叫者无需了解它。

它的最大缺点是,如果你有复杂的消息(例如Message.PLAYER_HIT ,它应该采用两个Player类型参数),你必须为参数编写包装类(在我们的例子中,它包含两个玩家)。 这可能非常繁琐。

就个人而言,我会以这种方式解决问题,因为我是一个强壮的人

public interface Message
{

    public static final Message instance = loadInstance();

    String you_died(Player player);

    String player_left(Player player, int score); 

    // etc. hundreds of them
}

// usage
String x = Message.instance.player_left(player, 10);

// one subclass per language
public class Message_jp implements Message
{
    public String you_died(Player player){ return player.lastName + "君,你地死啦死啦"; }
                                           // or whatever way you like to create a String
    // etc.
}

在运行时,您需要加载Message的正确子类。

static Message loadInstance()
{
    String lang = conf.get("language"); // e.g. "jp"
    Class clazz = Class.forName("Message_"+lang);  // Message_jp.class
    return clazz.newInstance();
}

这种方法将所有消息嵌入到类文件中,这应该没问题。

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

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

我跟着他的评论中发布的@Radiodef指南,一切都很顺利,直到我解释了如何将注释处理器与maven集成。 实际上,在使用maven并遵循该指南开始遇到一些困难之后,实际上,Apache Maven是用于此类注释处理的最佳依赖关系和构建管理工具。 因此,如果您还阅读该指南并使用maven,我建议您跳过第2部分。

但是,现在,它不是关于发生了哪些问题,而是要使其工作所需要做的事情:所需的maven依赖关系: org.apache.velocity:velocity:1.7:jar

项目设置稍有变化,因为包含源的实际项目将封装在根容器项目中。 这不是必需的,但它允许更清晰的项目结构和更可读的POM。

有4个POM:

  • RootProject
  • ActualProject
  • 注释
  • AnnotationProcessors

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

<modules>
    <module>ActualProject</module>
    <module>Annotations</module>
    <module>AnnotationProcessors</module>
</modules>

<!— Global dependencies can be configured here as well —>

ActualProject显然取决于Annotations工件以及AnnotationProcessors工件。 并且因为AnnotationProcessors工件依赖于Annotation项目,所以我们得到maven reactor的以下顺序:

  1. 注释
  2. AnnotationProcessors
  3. ActualProject

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

<plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <version>3.3</version>
     <configuration>
         <compilerArgs>
             <arg>-proc:none</arg>
         </compilerArgs>
     </configuration>
</plugin>

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

<plugin>
    <groupId>org.bsc.maven</groupId>
    <artifactId>maven-processor-plugin</artifactId>
    <version>2.2.4</version>
    <executions>
        <!-- Run annotation processors on src/main/java sources -->
        <execution>
            <id>process</id>
            <goals>
                <goal>process</goal>
            </goals>
            <phase>generate-sources</phase>
            <configuration>
                <outputDirectory>target/generated-sources</outputDirectory>
                <processors>
                    <processor>my.annotations.processors.MessageListProcessor</processor>
                </processors>
            </configuration>
        </execution>
    </executions>
</plugin>

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.9.1</version>
    <executions>
        <execution>
            <id>add-source</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>target/generated-sources</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

Annotation工件最重要的是包含value字段的注释,该字段是String类型,以及带注释的类也必须实现的接口。 枚举必须实现两种方法,显然是String getKey()String[] getParams() 在此之后,问题(消息)中的枚举扩展如下:

@MessageList("my.config.file.wrapper.type")
public enum Messages implements MessageInfo {

    NO_PERMISSION("no_permission"),
    YOU_DIED("you_died",                "score"),
    PLAYER_LEFT("player_left",          "player_name", "server_name");

    private String key;
    private String[] params;

    Messages(String key, String… params) {
        this.key = key;
        this.params = params;

    @Override
    public String getKey() { return key; }

    @Override
    public String[] getParams() { return params; }

}

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

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

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

Class<MessageInfo> messageInfoClass = (Class<MessageInfo>) Class.forName("my.annotations.MessageInfo");

是的,这确实是一个未经检查的强制转换,但我们使用一个常量字符串值,因此这不会导致ClassCastException。 无论如何,让我们编译带注释的类。 这意味着,带注释的类不会导入任何其他可能尚未编译的类。 它不应该因为它只是一个丰富的资源,技术上也可以是.properties文件。 同样,也是潜在的风险,而且,我们不关心,因为我们不会导入任何其他东西。

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

// The convertToPath method just returns "src/main/java/<pathWithSlashes>.java"
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(
    new File("ActualProject/" + convertToPath(element.getQualifiedName().toString())));

// The boolean here defines whether the last separator char should be cut off.
// We need to expand the class path so we might as well leave it there.
String classpath = getCurrentClasspath(false) +
    new File("Annotations/target/Annotations-version.jar").getAbsolutePath();

File outputDir = new File("ActualProject/target/classes/");
Iterable<String> arguments = Arrays.asList("-proc:none",
    "-d", outputDir.getAbsolutePath(),
    "-classpath", classpath);

boolean success = compiler.getTask(null, fileManager, null, arguments, null, compilationUnits).call();

fileManager.close();

最后,最后做的是检查成功的价值并返回它是否为假。 这是getCurrentClassPath方法:

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

现在,通过编译注释类,我们可以加载它:

URL classesURL = new URL("file://" + outputDir.getAbsolutePath() + "/");
// The current class loader serves as the parent class loader for the custom one.
// Obviously, it won’t find the compiled class.
URLClassLoader customCL = URLClassLoader.newInstance(new URL[]{classesURL}, classLoader);

Class<?> annotatedClass = customCL.loadClass(element.getQualifiedName().toString());

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

if (!Arrays.asList(annotatedClass.getInterfaces()).contains(messageInfoClass)) {
    raiseErrorAt(element, "Can only annotate subclasses of MessageInfo");
    continue;
}

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

MessageList annotation = element.getAnnotation(MessageList.class);
String locals = annotation.value();

// To get the package name, I used a while loop with an empty body. Does its job just fine.
Element enclosingElement = element;
while (!((enclosingElement = enclosingElement.getEnclosingElement()) instanceof PackageElement)) ;
String packageName = ((PackageElement) enclosingElement).getQualifiedName().toString();

ArrayList<Message> messages = new ArrayList<>();
for (Field field : annotatedClass.getDeclaredFields()) {
    if (!field.isEnumConstant()) continue;

    // Enum constants are static:
    Object value = field.get(null);
    MessageInfo messageInfo = messageInfoClass.cast(value);

    messages.add(new Message(field.getName(), messageInfo.getKey(), messageInfo.getParams()));
}

这里使用的Message类只是一个带有私有final字段和相应getter方法的数据类。 它可以在注释工件中找到,但我不确定在哪里放置它。 就是这样! 现在可以对Velocity Engine和Context进行实例化并传递值。 最后一块拼图是源的模板。 首先,我创建了3个变量但是特殊字符,因为我在将velocity的escape工具集成到我的项目中时非常失败......

#set ($doubleq = '"')
#set ($opencb = "{")
#set ($closecb = "}“)
package $package;

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

/**
 * This class was generated by the Annotation Processor for the project ActualProject.
 */
public abstract class Message {

#foreach ($message in $messages)

#set ($args = "")
#set ($replaces = "")
#foreach ($param in $message.params)
#set ($args = "${args}String $param, ")
#set ($replaces = "${replaces}.replace($doubleq$opencb$param$closecb$doubleq, $param)")
#end
#set ($endIndex = $args.length() - 2)
#if ($endIndex < 0)
#set ($endIndex = 0)
#end
#set ($args = $args.substring(0, $endIndex))
    public static final String ${message.name}($args) {
        return locals.getMessage("$message.key")$replaces;
    }

#end

    private static final $locals locals = ${locals}.getInstance();
}

乍一看,那套巨大的Velocity指令可能看起来有点奇怪,但它非常简单。 没有空行,因为它们实际上会生成,使生成的文件非常混乱。 那么做了什么? 我们遍历所有消息。 对于每条消息:

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

在文件的末尾,我们定义了Locals类的实例。 我的第一个计划是使用一个接口,但是效果不好,所以我只需要将该类作为单例。 第三次,这是另一个潜在的风险,第三次被忽略了同样的原因。

哦,你可能偶然发现的raiseErrorAt(Element,String)方法只是一个非常长时间调用processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element);的包装器processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, element);

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM