使用注解处理器进行代码替换

24

我正在尝试编写一个注解处理器,在类上插入方法和字段...但文档很匮乏。我没有进展,也不知道我是否正确地接近它。

处理环境提供了一个Filer 对象,其中有方便的方法来创建新的源代码和类文件。这些方法很好用,但是我试图弄清楚如何读取现有的源代码文件,它只提供了“getResource”。因此,在我的处理器实现中,我已经这样做:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
        for (TypeElement te : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(te)) {
                FileObject in_file = processingEnv.getFiler().getResource(
                    StandardLocation.SOURCE_PATH, "",
                    element.asType().toString().replace(".", "/") + ".java");

                FileObject out_file = processingEnv.getFiler().getResource(
                    StandardLocation.SOURCE_OUTPUT, "",
                    element.asType().toString().replace(".", "/") + ".java");

                //if (out_file.getLastModified() >= in_file.getLastModified()) continue;

                CharSequence data = in_file.getCharContent(false);

                data = transform(data); // run the macro processor

                JavaFileObject out_file2 = processingEnv.getFiler().createSourceFile(
                    element.asType().toString(), element);
                Writer w = out_file2.openWriter();
                w.append(data);
                w.close();
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
    }
    return true;
}

我的第一个难题是我无法摆脱这种感觉:element.asType().toString().replace(".", "/") + ".java"(获取限定类型名称并将其转换为包和源文件路径)不是解决问题的好方法。 API 的其余部分都过度设计,但似乎没有方便的方法来检索原始源代码。

真正的问题是编译器会因为输出目录中的第二个源文件而自动出现问题(“错误:重复的类”),现在我陷入了困境。

我已经写了其余部分 - 宏 lexer 和 parser 以及计算一些数据并插入字段值和方法的等等,但它作为初始步骤在编译器之外运行。除了原始文件不能具有 .java 扩展名(以防止编译器看到它们)之外,这很好地工作。然后我听说注释可以进行代码生成,我假设这会更加恰当和方便,但我找不到太多关于它的指导。


2
请见:http://techbitsfromsridhar.blogspot.ca/2013/02/java-compiler-plug-ins-in-java-8-use.html - Dave Jarvis
3个回答

21

注解处理器的意图是允许开发人员添加新类,而不是替换现有类。尽管如此,存在一个漏洞可以让你向现有类中添加代码。Project Lombok已经利用这个漏洞,将getter和setter(以及其他内容)添加到编译后的Java类中。

我所采用的方法来“替换”方法/字段是从输入类扩展或委托。这样可以允许你覆盖/转移对目标类的调用。

因此,如果这是你的输入类:

InputImpl.java:

public class InputImpl implements Input{
    public void foo(){
        System.out.println("foo");
    }
    public void bar(){
        System.out.println("bar");
    }
}

您可以生成以下内容来“替换”它:

InputReplacementImpl.java:

public class InputReplacementImpl implements Input{

    private Input delegate;

    //setup delegate....

    public void foo(){
        System.out.println("foo replacement");
    }
    public void bar(){
        delegate.bar();
    }
}

这引出了一个问题,如何引用InputReplacementImpl而不是InputImpl。您可以生成一些代码来执行包装,或者简单地调用预计将生成的代码的构造函数。

我不太确定您的问题是什么,但我希望这能解决您的问题。


啊哈!我想我以前见过Lombok,这也是我没有理由认为这是不可能的部分原因。这个答案解释了编译器错误并给了我几个选择。我没有渴望陷入AST hack中,因为目前简单的正则表达式可以为我进行代码查找和替换。在我的情况下,委托给类并不可行,因为原始类不完整且无法编译。就我所看到的,注释API的这个愚蠢小限制意味着它对我没有用处,尽管它很复杂。但现在我知道该怎么做了:不使用注释!非常感谢您的帮助。 - Boann
@JohnEricksen 我很好奇如何引用生成的类。要想简单地使用构造函数,注解类应该先编译。如果没有编译,IDE会报告没有类定义错误。至于封装,怎么做呢? - yk42b
@yk42b,只要类路径设置正确并且注解处理器正在运行,生成的类就可以在IDE中引用。 - John Ericksen

1

0

我不确定这个提示是否符合您的需求,但是您的想法可以与依赖注入(例如Spring)一起使用。

  1. 为类型和保留“SOURCE”的自定义注释创建一个注释。
  2. 在您的注释处理器中对代码进行所需更改,并将Spring的@Component注释添加到您生成的Java类型中,并将其保存为原始类型的子类型,类名与原始类型不同。
  3. 当Spring创建上下文时,它将基于您的处理结果而不是原始结果加载类。

相反,Lombok直接操作抽象源树,这比生成源代码要复杂得多。问题在于,Java编译器模块被公开(Jigsaw)。

另一种选择可能是使用像ByteBuddy这样的字节码API,在运行时代理原始类。如果您不想编写用于实例化类的代码,则还需要依赖注入。这里的魔法是配置类中类型集合的自动装配字段。如果您对此感兴趣,请告诉我。然后我会在这里添加更多细节。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接