【问题标题】:Auto generate replace methods自动生成替换方法
【发布时间】:2019-08-08 20:01:14
【问题描述】:

在为我正在制作的应用程序创建语言文件时,我遇到了很多样板代码。我目前有一个包含所有语言字符串的类,然后我使用反射将这些字符串写入文件。

我经常遇到的是,我的字符串中有某些占位符要替换,例如,我可能有这样的字符串:

public static String USER_INFO = "Username: %name% money: %balance%";

我想要实现的是生成一些基于注释的方法,比如我可以使用 lombok 生成 getter/setter 和其他方法。基于上面的字符串,我会有一个名为 Arguments(正确地应该被命名为 Replacers 或更有意义的东西)的注释,如下所示:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface Arguments {
  String[] value();
}

我想做的是像这样添加注释:

@Arguments(
        value = {"%balance%", "%name%"}
)
public static String USER_INFO = "Username: %name% - money: %balance%";

并自动生成以下替换方法:

public static String USER_INFONameReplacement(String name) {
    return USER_INFO.replace("%name%", name);
}
public static String USER_INFOAllReplacement(String name, String balance) {
    return USER_INFO.replace("%name%", name).replace("%balance%", balance);
}
public static String USER_INFOBalanceReplacement(String balance) {
    return USER_INFO.replace("%balance%", balance);
}

经过一番搜索后,我最终尝试在这样的类中实现 AbstractProcessor:

@SupportedAnnotationTypes(
    {"io.github.freakyville.configHelper.annotations.Arguments"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class SuggestProcessor extends AbstractProcessor {

@Override
public synchronized void init(ProcessingEnvironment env) {
}

@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {
    for (TypeElement annoation : annoations) {
        Set<? extends Element> annotatedElements = env.getElementsAnnotatedWith(annoation);
        Map<Boolean, List<Element>> annotatedFields = annotatedElements.stream().collect(
                Collectors.partitioningBy(element ->
                        ((ArrayType) element.asType()).getComponentType().getClass().equals(PrimitiveType.class)));
        List<Element> setters = annotatedFields.get(true);
        if (setters.isEmpty()) {
            continue;
        }
        String className = ((TypeElement) setters.get(0)
                .getEnclosingElement()).getQualifiedName().toString();

        Map<String, List<String>> setterMap = setters.stream().collect(Collectors.toMap(
                setter -> setter.getSimpleName().toString(),
                setter -> Arrays.asList(setter.getAnnotation(Arguments.class).value()))
        );
        try {
            writeBuilderFile(className, setterMap);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return true;
}

private void writeBuilderFile(
        String className, Map<String, List<String>> setterMap)
        throws IOException {

    String packageName = null;
    int lastDot = className.lastIndexOf('.');
    if (lastDot > 0) {
        packageName = className.substring(0, lastDot);
    }

    String builderSimpleClassName = className
            .substring(lastDot + 1);

    JavaFileObject builderFile = processingEnv.getFiler()
            .createSourceFile(className);

    try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {

        if (packageName != null) {
            out.print("package ");
            out.print(packageName);
            out.println(";");
            out.println();
        }

        out.print("public class ");
        out.print(builderSimpleClassName);
        out.println(" {");
        out.println();

        setterMap.forEach((key, orgArgNames) -> {

            for (int i = 0; i < orgArgNames.size(); i++) {
                List<String> subList = orgArgNames.subList(0, i + 1);
                List<String> argNames = subList.stream().map(v -> v.replace("%", "") + "Replacement").collect(Collectors.toList());
                List<String> argsWithTypes = argNames.stream().map(v -> "String " + v).collect(Collectors.toList());
                String argumentList = "(" + String.join("", argsWithTypes).substring(0, argsWithTypes.size() - 3) + ")";
                String methodName;

                if (orgArgNames.size() <= 1) {
                    methodName = key + "Replace" + subList.stream().map(v -> v.replace("%", "")).collect(Collectors.joining(""));
                } else {
                    methodName = key + "Replace" + subList.stream().map(v -> v.replace("%", "").substring(0, 1).toUpperCase() + v.substring(1)).collect(Collectors.joining(""));
                }

                out.print("    public static ");
                out.print(methodName);
                out.print(argumentList);
                out.println("{");
                StringBuilder replaceSB = new StringBuilder();
                replaceSB.append(key);
                for (int i1 = 0; i1 < subList.size(); i1++) {
                    replaceSB
                            .append(".replace(")
                            .append("\"")
                            .append(subList.get(i))
                            .append("\"")
                            .append(",")
                            .append(argNames.get(i))
                            .append(")");

                }
                String replace = replaceSB.toString();
                out.println("return " + replace + ";");
                out.println("}");
                out.println("");
            }
        });

        out.println("}");
    }
}


}

但我似乎无法让它注册?

所以我的第一个问题是,如果我想实现这一目标,AbstractProcessor 是否可行?如果不是那怎么办?如果是,那为什么不注册?我正在使用 IntelliJ 并进入设置 -> 构建 -> 编译器并将注释处理器更改为启用并将处理器路径设置为我的 SuggestProcessor

【问题讨论】:

  • 这可能是ByteBuddy 的一个很好的用例。 This 回答可能会有所帮助。

标签: java annotations code-generation


【解决方案1】:

Java 注释处理 (APT) 插件旨在生成基于其他类的代码。这些类最终在生成的源文件夹中,然后也被编译。这些 APT 插件是从类路径/构建工具配置中发现的,并且也由 IntelliJ 编译器运行。请记住:APT 用于生成源代码生成,而不是用于替换现有类。 Lombok 仍然能够这样做的唯一原因是因为它们非常深入地侵入编译器,从而能够操纵正在编译的类的 AST。

由于这种方法在未来的 Java 版本中存在很大争议且容易出错,因此任何人都不太可能尝试构建基于 APT 的类替换框架或能够做到这一点的 Lombok 扩展(不是因为 Lombok 是唯一可以被视为此类 APT 使用的“框架”的工具,而且 Lombok 本身根本不是以可扩展的方式构建的)。

总结:APT 可能是要走的路,但您的处理器必须创建一个新类,而不是尝试修改现有类。

如何创建注释处理器的示例您可以查看以下存储库:https://github.com/galberola/java-apt-simple-example

我不确定为什么您当前的注释处理器与您的编译器没有正确关联。如果您使用的是 Maven,您可以尝试在本地为您的处理器安装工件,并将其作为编译依赖项添加到您的其他项目。不要忘记使用编译器将该类注册为注释处理器,我引用的示例项目在这里执行此操作:https://github.com/galberola/java-apt-simple-example/blob/master/example/pom.xml#L29-L31。相同的配置也可以应用于其他构建系统。

在Java中没有真正的方法来修改编译下的类,所以如果你真的必须在同一个类中拥有该方法,那么不幸的是,这意味着它无法做到。

【讨论】:

    【解决方案2】:

    您可以修改抽象语法树 (AST),而不是实际创建文件并写入文件,就像 Lombok 所做的那样。不建议这样做,不同的编译器以不同的方式实现 AST,但您可以从 github (https://github.com/rzwitserloot/lombok) 扩展 Lombok 源代码,并根据需要制作注释处理程序。但是,它有点难,所以请确保您确实需要它。

    抱歉,我没有正确阅读您的问题。要注册它,您需要在使用注释和注释处理器的项目中创建一个 META-INF\services 目录。在该目录中,创建一个名为“javax.annotation.processing.Processor”的 txt 文件,其中包含处理器的名称,例如 mypackage.SuggestProcessor。如果您决定使用 java 9,您还可以在 module-info 文件中声明处理器。处理器的模块必须包含“provides javax.annotation.processing.Processor with something.SuggestProcessor”,使用注解的模块必须包含“uses javax.annotation.processing.Processor”。这就是 javac 注册注解处理器的方式。

    【讨论】:

    • 感谢您的回答 - 但是我可以在 intellij 中扩展 lombok 并自己创建一个注释处理程序吗?我在这里读到:baeldung.com/lombok-custom-annotation 它需要由 intellij 插件支持,这对我来说不是这样吗?
    猜你喜欢
    • 2017-08-22
    • 1970-01-01
    • 1970-01-01
    • 2014-08-04
    • 2011-10-23
    • 1970-01-01
    • 1970-01-01
    • 2016-01-04
    • 2020-12-20
    相关资源
    最近更新 更多