BAEL-1290 Creating a Java Compiler Plugin
* a sample Javac plugin * a test framework for Javac plugins * sample Javac plugin's tests
This commit is contained in:
parent
d97456ead8
commit
8e5e0e3bb3
|
@ -216,6 +216,13 @@
|
|||
<artifactId>spring-web</artifactId>
|
||||
<version>4.3.4.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun</groupId>
|
||||
<artifactId>tools</artifactId>
|
||||
<version>1.8.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${java.home}/../lib/tools.jar</systemPath>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.CLASS)
|
||||
@Target({ElementType.PARAMETER})
|
||||
public @interface Positive {
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.source.util.*;
|
||||
import com.sun.tools.javac.api.BasicJavacTask;
|
||||
import com.sun.tools.javac.code.TypeTag;
|
||||
import com.sun.tools.javac.tree.JCTree;
|
||||
import com.sun.tools.javac.tree.TreeMaker;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import com.sun.tools.javac.util.Name;
|
||||
import com.sun.tools.javac.util.Names;
|
||||
|
||||
import javax.tools.JavaCompiler;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sun.tools.javac.util.List.nil;
|
||||
|
||||
/**
|
||||
* A {@link JavaCompiler javac} plugin which inserts {@code >= 0} checks into resulting {@code *.class} files
|
||||
* for numeric method parameters marked by {@link Positive}
|
||||
*/
|
||||
public class SampleJavacPlugin implements Plugin {
|
||||
|
||||
public static final String NAME = "MyPlugin";
|
||||
|
||||
private static Set<String> TARGET_TYPES = new HashSet<>(Arrays.asList(
|
||||
// Use only primitive types for simplicity
|
||||
byte.class.getName(), short.class.getName(), char.class.getName(), int.class.getName(),
|
||||
long.class.getName(), float.class.getName(), double.class.getName()
|
||||
));
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JavacTask task, String... args) {
|
||||
Context context = ((BasicJavacTask) task).getContext();
|
||||
task.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void started(TaskEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
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()) {
|
||||
// There is a possible case that more than one argument is marked by @Positive,
|
||||
// as the checks are added to the method's body beginning, we process parameters RTL
|
||||
// to ensure correct order.
|
||||
Collections.reverse(parametersToInstrument);
|
||||
parametersToInstrument.forEach(p -> addCheck(method, p, context));
|
||||
}
|
||||
// There is a possible case that there is a nested class declared in a method's body,
|
||||
// hence, we want to proceed with method body AST as well.
|
||||
return super.visitMethod(method, v);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static JCTree.JCIf createCheck(VariableTree parameter, Context context) {
|
||||
TreeMaker factory = TreeMaker.instance(context);
|
||||
Names symbolsTable = Names.instance(context);
|
||||
String parameterName = parameter.getName().toString();
|
||||
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";
|
||||
Name parameterId = symbolsTable.fromString(parameterName);
|
||||
return factory.at(((JCTree) parameter).pos).If(
|
||||
factory.Parens(
|
||||
factory.Binary(
|
||||
JCTree.Tag.LE,
|
||||
factory.Ident(parameterId),
|
||||
factory.Literal(TypeTag.INT, 0))
|
||||
),
|
||||
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
|
||||
)
|
||||
)
|
||||
)),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
com.baeldung.javac.SampleJavacPlugin
|
|
@ -0,0 +1,42 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class SampleJavacPluginIntegrationTest {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void givenInt_whenZero_thenThrowsException() throws Throwable {
|
||||
compileAndRun(int.class,0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenInt_whenPositive_thenSuccess() throws Throwable {
|
||||
assertEquals(1, compileAndRun(int.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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import javax.tools.SimpleJavaFileObject;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
|
||||
/** Holds compiled byte code in a byte array */
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import javax.tools.*;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Adapts {@link SimpleClassFile} to the {@link JavaCompiler} */
|
||||
public class SimpleFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
|
||||
|
||||
private final List<SimpleClassFile> compiled = new ArrayList<>();
|
||||
|
||||
public SimpleFileManager(StandardJavaFileManager delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return compiled binaries processed by the current class
|
||||
*/
|
||||
public List<SimpleClassFile> getCompiled() {
|
||||
return compiled;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import javax.tools.SimpleJavaFileObject;
|
||||
import java.net.URI;
|
||||
|
||||
/** Exposes given test source to the compiler. */
|
||||
public class SimpleSourceFile extends SimpleJavaFileObject {
|
||||
|
||||
private final 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.ToolProvider;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.baeldung.javac;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue