Creating a Java Compiler Plugin – 创建Java编译器插件

最后修改: 2017年 11月 14日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Java 8 provides an API for creating Javac plugins. Unfortunately, it’s hard to find good documentation for it.

Java 8提供了一个用于创建Javac插件的API。不幸的是,很难找到好的文档。

In this article, we’re going to show the whole process of creating a compiler extension which adds custom code to *.class files.

在这篇文章中,我们将展示创建一个编译器扩展的整个过程,该扩展将自定义代码添加到*.class文件。

2. Setup

2.设置

First, we need to add JDK’s tools.jar as a dependency for our project:

首先,我们需要添加JDK的tools.jar作为我们项目的一个依赖。

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8.0</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

Every compiler extension is a class which implements com.sun.source.util.Plugin interface. Let’s create it in our example:

每个编译器扩展都是一个实现com.sun.source.util.Plugin接口的类。让我们在我们的例子中创建它。

Let’s create it in our example:

让我们在我们的例子中创建它。

public class SampleJavacPlugin implements Plugin {

    @Override
    public String getName() {
        return "MyPlugin";
    }

    @Override
    public void init(JavacTask task, String... args) {
        Context context = ((BasicJavacTask) task).getContext();
        Log.instance(context)
          .printRawLines(Log.WriterKind.NOTICE, "Hello from " + getName());
    }
}

For now, we’re just printing “Hello” to ensure that our code is successfully picked up and included in the compilation.

现在,我们只是打印 “Hello”,以确保我们的代码被成功接收并包含在编译中。

Our end goal will be to create a plugin that adds runtime checks for every numeric argument marked with a given annotation, and throw an exception if the argument doesn’t match a condition.

我们的最终目标是创建一个插件,为每个标有特定注释的数字参数添加运行时检查,并在参数不符合条件时抛出一个异常。

There’s one more necessary step to make the extension discoverable by Javac: it should be exposed through the ServiceLoader framework.

还有一个必要的步骤,使扩展可以被Javac发现: 它应该通过ServiceLoader框架公开。

To achieve this, we need to create a file named com.sun.source.util.Plugin with content which is our plugin’s fully qualified class name (com.baeldung.javac.SampleJavacPlugin) and place it in the META-INF/services directory.

为了实现这一目标,我们需要创建一个名为com.sun.source.util.Plugin的文件,其内容是我们的插件的完全限定类名(com.baeldung.javac.SampleJavacPlugin)并将其放在META-INF/services目录下。

After that, we can call Javac with the -Xplugin:MyPlugin switch:

之后,我们可以用Xplugin:MyPlugin开关调用Javac

baeldung/tutorials$ javac -cp ./core-java/target/classes -Xplugin:MyPlugin ./core-java/src/main/java/com/baeldung/javac/TestClass.java
Hello from MyPlugin

Note that we must always use a String returned from the plugin’s getName() method as a -Xplugin option value.

请注意,我们必须始终使用从插件的getName()方法返回的String作为Xplugin选项值

3. Plugin Lifecycle

3.插件的生命周期

A plugin is called by the compiler only once, through the init() method.

一个插件只被编译器调用一次,通过init()方法。

To be notified of subsequent events, we have to register a callback. These arrive before and after every processing stage per source file:

为了得到后续事件的通知,我们必须注册一个回调。这些回调在每个源文件的每个处理阶段之前和之后到达。

  • PARSE – builds an Abstract Syntax Tree (AST)
  • ENTER – source code imports are resolved
  • ANALYZE – parser output (an AST) is analyzed for errors
  • GENERATE – generating binaries for the target source file

There are two more event kinds – ANNOTATION_PROCESSING and ANNOTATION_PROCESSING_ROUND but we’re not interested in them here.

还有两种事件–ANNOTATION_PROCESSINGANNOTATION_PROCESSING_ROUND,但我们在这里对它们不感兴趣。

For example, when we want to enhance compilation by adding some checks based on source code info, it’s reasonable to do that at the PARSE finished event handler:

例如,当我们想通过添加一些基于源代码信息的检查来加强编译时,在PARSE finished事件处理程序中这样做是合理的。

public void init(JavacTask task, String... args) {
    task.addTaskListener(new TaskListener() {
        public void started(TaskEvent e) {
        }

        public void finished(TaskEvent e) {
            if (e.getKind() != TaskEvent.Kind.PARSE) {
                return;
            }
            // Perform instrumentation
        }
    });
}

4. Extract AST Data

4.提取AST数据

We can get an AST generated by the Java compiler through the TaskEvent.getCompilationUnit(). Its details can be examined through the TreeVisitor interface.

我们可以通过TaskEvent.getCompilationUnit()获得由Java编译器生成的AST。其细节可以通过TreeVisitor接口检查。

Note that only a Tree element, for which the accept() method is called, dispatches events to the given visitor.

请注意,只有调用了accept()方法的Tree元素才会向给定的访问者分派事件。

For example, when we execute ClassTree.accept(visitor), only visitClass() is triggered; we can’t expect that, say, visitMethod() is also activated for every method in the given class.

例如,当我们执行ClassTree.accept(visitor)时,只有visitClass()被触发;我们不能期望,比如说,visitMethod()也被激活,用于指定类中的每个方法。

We can use TreeScanner to overcome the problem:

我们可以使用TreeScanner来克服这个问题。

public void finished(TaskEvent e) {
    if (e.getKind() != TaskEvent.Kind.PARSE) {
        return;
    }
    e.getCompilationUnit().accept(new TreeScanner<Void, Void>() {
        @Override
        public Void visitClass(ClassTree node, Void aVoid) {
            return super.visitClass(node, aVoid);
        }

        @Override
        public Void visitMethod(MethodTree node, Void aVoid) {
            return super.visitMethod(node, aVoid);
        }
    }, null);
}

In this example, it’s necessary to call super.visitXxx(node, value) to recursively process the current node’s children.

在这个例子中,需要调用super.visitXxx(node, value)来递归处理当前节点的子节点。

5. Modify AST

5.修改AST

To showcase how we can modify the AST, we’ll insert runtime checks for all numeric arguments marked with a @Positive annotation.

为了展示我们如何修改AST,我们将为所有标有@Positive注释的数字参数插入运行时检查。

This is a simple annotation that can be applied to method parameters:

这是一个简单的注解,可以应用于方法参数。

@Documented
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.PARAMETER})
public @interface Positive { }

Here’s an example of using the annotation:

下面是一个使用注释的例子。

public void service(@Positive int i) { }

In the end, we want the bytecode to look as if it’s compiled from a source like this:

最后,我们希望字节码看起来就像从这样的源码编译出来的一样。

public void service(@Positive int i) {
    if (i <= 0) {
        throw new IllegalArgumentException("A non-positive argument ("
          + i + ") is given as a @Positive parameter 'i'");
    }
}

What this means is that we want an IllegalArgumentException to be thrown for every argument marked with @Positive which is equal or less than 0.

这意味着我们希望对每个标有@Positive的参数抛出一个IllegalArgumentException,该参数等于或小于0。

5.1. Where to Instrument

5.1.在哪里使用仪器

Let’s find out how we can locate target places where the instrumentation should be applied:

让我们来看看如何定位应该应用仪器的目标地点。

private static Set<String> TARGET_TYPES = Stream.of(
  byte.class, short.class, char.class, 
  int.class, long.class, float.class, double.class)
 .map(Class::getName)
 .collect(Collectors.toSet());

For simplicity, we’ve only added primitive numeric types here.

为了简单起见,我们在这里只增加了原始的数字类型。

Next, let’s define a shouldInstrument() method that checks if the parameter has a type in the TARGET_TYPES set as well as the @Positive annotation:

接下来,让我们定义一个shouldInstrument()方法,检查参数是否有TARGET_TYPES集合中的类型,以及@Positive注解。

private boolean shouldInstrument(VariableTree parameter) {
    return TARGET_TYPES.contains(parameter.getType().toString())
      && parameter.getModifiers().getAnnotations().stream()
      .anyMatch(a -> Positive.class.getSimpleName()
        .equals(a.getAnnotationType().toString()));
}

Then we’ll continue the finished() method in our SampleJavacPlugin class with applying a check to all parameters that fulfill our conditions:

然后,我们将继续执行finished()方法,在我们的SampleJavacPlugin类中对所有满足条件的参数进行检查。

public void finished(TaskEvent e) {
    if (e.getKind() != TaskEvent.Kind.PARSE) {
        return;
    }
    e.getCompilationUnit().accept(new TreeScanner<Void, Void>() {
        @Override
        public Void visitMethod(MethodTree method, Void v) {
            List<VariableTree> parametersToInstrument
              = method.getParameters().stream()
              .filter(SampleJavacPlugin.this::shouldInstrument)
              .collect(Collectors.toList());
            
              if (!parametersToInstrument.isEmpty()) {
                Collections.reverse(parametersToInstrument);
                parametersToInstrument.forEach(p -> addCheck(method, p, context));
            }
            return super.visitMethod(method, v);
        }
    }, null);

In this example, we’ve reversed the parameters list because there’s a possible case that more than one argument is marked by @Positive. As every check is added as the very first method instruction, we process them RTL to ensure the correct order.

在这个例子中,我们颠倒了参数列表,因为有可能出现不止一个参数被@Positive.标记的情况。由于每个检查都是作为第一个方法指令添加的,我们对它们进行RTL处理以确保顺序正确。

5.2. How to Instrument

5.2.如何使用仪器

The problem is that “read AST” lays in the public API area, while “modify AST” operations like “add null-checks” are a private API.

问题是,”读取AST “属于公共API领域,而 “修改AST “的操作,如 “添加null-checks “是私人API。

To address this, we’ll create new AST elements through a TreeMaker instance.

为了解决这个问题,我们将通过一个TreeMaker实例创建新的AST元素。

First, we need to obtain a Context instance:

首先,我们需要获得一个Context实例。

@Override
public void init(JavacTask task, String... args) {
    Context context = ((BasicJavacTask) task).getContext();
    // ...
}

Then, we can obtain the TreeMarker object through the TreeMarker.instance(Context) method.

然后,我们可以通过TreeMarker.instance(Context)方法获得TreeMarker对象。

Now we can build new AST elements, e.g., an if expression can be constructed by a call to TreeMaker.If():

现在我们可以构建新的AST元素,例如,一个if表达式可以通过调用TreeMaker.If()来构建。

private static JCTree.JCIf createCheck(VariableTree parameter, Context context) {
    TreeMaker factory = TreeMaker.instance(context);
    Names symbolsTable = Names.instance(context);
        
    return factory.at(((JCTree) parameter).pos)
      .If(factory.Parens(createIfCondition(factory, symbolsTable, parameter)),
        createIfBlock(factory, symbolsTable, parameter), 
        null);
}

Please note that we want to show the correct stack trace line when an exception is thrown from our check. That’s why we adjust the AST factory position before creating new elements through it with factory.at(((JCTree) parameter).pos).

请注意,当我们的检查出现异常时,我们希望显示正确的堆栈跟踪线。这就是为什么我们在通过AST工厂创建新元素之前用factory.at(((JCTree) parameter).pos)调整AST工厂的位置。

The createIfCondition() method builds the “parameterId < 0″ if condition:

createIfCondition()方法建立了”parameterId < 0″ if条件。

private static JCTree.JCBinary createIfCondition(TreeMaker factory, 
  Names symbolsTable, VariableTree parameter) {
    Name parameterId = symbolsTable.fromString(parameter.getName().toString());
    return factory.Binary(JCTree.Tag.LE, 
      factory.Ident(parameterId), 
      factory.Literal(TypeTag.INT, 0));
}

Next, the createIfBlock() method builds a block that returns an IllegalArgumentException:

接下来,createIfBlock()方法建立了一个块,返回一个IllegalArgumentException:

private static JCTree.JCBlock createIfBlock(TreeMaker factory, 
  Names symbolsTable, VariableTree parameter) {
    String parameterName = parameter.getName().toString();
    Name parameterId = symbolsTable.fromString(parameterName);
        
    String errorMessagePrefix = String.format(
      "Argument '%s' of type %s is marked by @%s but got '", 
      parameterName, parameter.getType(), Positive.class.getSimpleName());
    String errorMessageSuffix = "' for it";
        
    return factory.Block(0, com.sun.tools.javac.util.List.of(
      factory.Throw(
        factory.NewClass(null, nil(), 
          factory.Ident(symbolsTable.fromString(
            IllegalArgumentException.class.getSimpleName())),
            com.sun.tools.javac.util.List.of(factory.Binary(JCTree.Tag.PLUS, 
            factory.Binary(JCTree.Tag.PLUS, 
              factory.Literal(TypeTag.CLASS, errorMessagePrefix), 
              factory.Ident(parameterId)), 
              factory.Literal(TypeTag.CLASS, errorMessageSuffix))), null))));
}

Now that we’re able to build new AST elements, we need to insert them into the AST prepared by the parser. We can achieve this by casting public API elements to private API types:

现在我们能够构建新的AST元素了,我们需要将它们插入由解析器准备的AST中。我们可以通过将publicAPI元素转换为privateAPI类型来实现。

private void addCheck(MethodTree method, VariableTree parameter, Context context) {
    JCTree.JCIf check = createCheck(parameter, context);
    JCTree.JCBlock body = (JCTree.JCBlock) method.getBody();
    body.stats = body.stats.prepend(check);
}

6. Testing the Plugin

6.测试插件

We need to be able to test our plugin. It involves the following:

我们需要能够测试我们的插件。它涉及以下内容。

  • compile the test source
  • run the compiled binaries and ensure that they behave as expected

For this, we need to introduce a few auxiliary classes.

为此,我们需要引入一些辅助类。

SimpleSourceFile exposes the given source file’s text to the Javac:

SimpleSourceFileJavac公开给定的源文件的文本。

public class SimpleSourceFile extends SimpleJavaFileObject {
    private String content;

    public SimpleSourceFile(String qualifiedClassName, String testSource) {
        super(URI.create(String.format(
          "file://%s%s", qualifiedClassName.replaceAll("\\.", "/"),
          Kind.SOURCE.extension)), Kind.SOURCE);
        content = testSource;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return content;
    }
}

SimpleClassFile holds the compilation result as a byte array:

SimpleClassFile以字节数组形式保存编译结果。

public class SimpleClassFile extends SimpleJavaFileObject {

    private ByteArrayOutputStream out;

    public SimpleClassFile(URI uri) {
        super(uri, Kind.CLASS);
    }

    @Override
    public OutputStream openOutputStream() throws IOException {
        return out = new ByteArrayOutputStream();
    }

    public byte[] getCompiledBinaries() {
        return out.toByteArray();
    }

    // getters
}

SimpleFileManager ensures the compiler uses our bytecode holder:

SimpleFileManager确保编译器使用我们的字节码持有人。

public class SimpleFileManager
  extends ForwardingJavaFileManager<StandardJavaFileManager> {

    private List<SimpleClassFile> compiled = new ArrayList<>();

    // standard constructors/getters

    @Override
    public JavaFileObject getJavaFileForOutput(Location location,
      String className, JavaFileObject.Kind kind, FileObject sibling) {
        SimpleClassFile result = new SimpleClassFile(
          URI.create("string://" + className));
        compiled.add(result);
        return result;
    }

    public List<SimpleClassFile> getCompiled() {
        return compiled;
    }
}

Finally, all of that is bound to the in-memory compilation:

最后,所有这些都被绑定到内存中的编译。

public class TestCompiler {
    public byte[] compile(String qualifiedClassName, String testSource) {
        StringWriter output = new StringWriter();

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        SimpleFileManager fileManager = new SimpleFileManager(
          compiler.getStandardFileManager(null, null, null));
        List<SimpleSourceFile> compilationUnits 
          = singletonList(new SimpleSourceFile(qualifiedClassName, testSource));
        List<String> arguments = new ArrayList<>();
        arguments.addAll(asList("-classpath", System.getProperty("java.class.path"),
          "-Xplugin:" + SampleJavacPlugin.NAME));
        JavaCompiler.CompilationTask task 
          = compiler.getTask(output, fileManager, null, arguments, null,
          compilationUnits);
        
        task.call();
        return fileManager.getCompiled().iterator().next().getCompiledBinaries();
    }
}

After that, we need only to run the binaries:

之后,我们只需要运行二进制文件。

public class TestRunner {

    public Object run(byte[] byteCode, String qualifiedClassName, String methodName,
      Class<?>[] argumentTypes, Object... args) throws Throwable {
        ClassLoader classLoader = new ClassLoader() {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                return defineClass(name, byteCode, 0, byteCode.length);
            }
        };
        Class<?> clazz;
        try {
            clazz = classLoader.loadClass(qualifiedClassName);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Can't load compiled test class", e);
        }

        Method method;
        try {
            method = clazz.getMethod(methodName, argumentTypes);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(
              "Can't find the 'main()' method in the compiled test class", e);
        }

        try {
            return method.invoke(null, args);
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }
    }
}

A test might look like this:

一个测试可能看起来像这样。

public class SampleJavacPluginTest {

    private static final String CLASS_TEMPLATE
      = "package com.baeldung.javac;\n\n" +
        "public class Test {\n" +
        "    public static %1$s service(@Positive %1$s i) {\n" +
        "        return i;\n" +
        "    }\n" +
        "}\n" +
        "";

    private TestCompiler compiler = new TestCompiler();
    private TestRunner runner = new TestRunner();

    @Test(expected = IllegalArgumentException.class)
    public void givenInt_whenNegative_thenThrowsException() throws Throwable {
        compileAndRun(double.class,-1);
    }
    
    private Object compileAndRun(Class<?> argumentType, Object argument) 
      throws Throwable {
        String qualifiedClassName = "com.baeldung.javac.Test";
        byte[] byteCode = compiler.compile(qualifiedClassName, 
          String.format(CLASS_TEMPLATE, argumentType.getName()));
        return runner.run(byteCode, qualifiedClassName, 
        "service", new Class[] {argumentType}, argument);
    }
}

Here we’re compiling a Test class with a service() method that has a parameter annotated with @Positive. Then, we’re running the Test class by setting a double value of -1 for the method parameter.

在这里,我们正在编译一个带有service()方法的Test类,该方法有一个用@Positive.注解的参数。然后,我们通过为方法参数设置-1的双重值来运行Test类。

As a result of running the compiler with our plugin, the test will throw an IllegalArgumentException for the negative parameter.

由于用我们的插件运行编译器,测试将抛出一个IllegalArgumentException的负参数。

7. Conclusion

7.结论

In this article, we’ve shown the full process of creating, testing and running a Java Compiler plugin.

在这篇文章中,我们展示了创建、测试和运行一个Java编译器插件的全部过程。

The full source code of the examples can be found over on GitHub.

示例的完整源代码可以在GitHub上找到over