diff --git a/core-java/pom.xml b/core-java/pom.xml
index cea5e9b3ec..dbf61c3acf 100644
--- a/core-java/pom.xml
+++ b/core-java/pom.xml
@@ -216,6 +216,13 @@
spring-web
4.3.4.RELEASE
+
+ com.sun
+ tools
+ 1.8.0
+ system
+ ${java.home}/../lib/tools.jar
+
diff --git a/core-java/src/main/java/com/baeldung/javac/Positive.java b/core-java/src/main/java/com/baeldung/javac/Positive.java
new file mode 100644
index 0000000000..443b866fea
--- /dev/null
+++ b/core-java/src/main/java/com/baeldung/javac/Positive.java
@@ -0,0 +1,9 @@
+package com.baeldung.javac;
+
+import java.lang.annotation.*;
+
+@Documented
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.PARAMETER})
+public @interface Positive {
+}
diff --git a/core-java/src/main/java/com/baeldung/javac/SampleJavacPlugin.java b/core-java/src/main/java/com/baeldung/javac/SampleJavacPlugin.java
new file mode 100644
index 0000000000..c85b04f0cf
--- /dev/null
+++ b/core-java/src/main/java/com/baeldung/javac/SampleJavacPlugin.java
@@ -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 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() {
+ @Override
+ public Void visitMethod(MethodTree method, Void v) {
+ List 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
+ );
+ }
+}
diff --git a/core-java/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/core-java/src/main/resources/META-INF/services/com.sun.source.util.Plugin
new file mode 100644
index 0000000000..91fb6eb3b0
--- /dev/null
+++ b/core-java/src/main/resources/META-INF/services/com.sun.source.util.Plugin
@@ -0,0 +1 @@
+com.baeldung.javac.SampleJavacPlugin
\ No newline at end of file
diff --git a/core-java/src/test/java/com/baeldung/javac/SampleJavacPluginIntegrationTest.java b/core-java/src/test/java/com/baeldung/javac/SampleJavacPluginIntegrationTest.java
new file mode 100644
index 0000000000..b877038add
--- /dev/null
+++ b/core-java/src/test/java/com/baeldung/javac/SampleJavacPluginIntegrationTest.java
@@ -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);
+ }
+}
diff --git a/core-java/src/test/java/com/baeldung/javac/SimpleClassFile.java b/core-java/src/test/java/com/baeldung/javac/SimpleClassFile.java
new file mode 100644
index 0000000000..2c8e66e6e3
--- /dev/null
+++ b/core-java/src/test/java/com/baeldung/javac/SimpleClassFile.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/core-java/src/test/java/com/baeldung/javac/SimpleFileManager.java b/core-java/src/test/java/com/baeldung/javac/SimpleFileManager.java
new file mode 100644
index 0000000000..346f240754
--- /dev/null
+++ b/core-java/src/test/java/com/baeldung/javac/SimpleFileManager.java
@@ -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 {
+
+ private final List 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 getCompiled() {
+ return compiled;
+ }
+}
\ No newline at end of file
diff --git a/core-java/src/test/java/com/baeldung/javac/SimpleSourceFile.java b/core-java/src/test/java/com/baeldung/javac/SimpleSourceFile.java
new file mode 100644
index 0000000000..9287b1a0dd
--- /dev/null
+++ b/core-java/src/test/java/com/baeldung/javac/SimpleSourceFile.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/core-java/src/test/java/com/baeldung/javac/TestCompiler.java b/core-java/src/test/java/com/baeldung/javac/TestCompiler.java
new file mode 100644
index 0000000000..ee40e563a3
--- /dev/null
+++ b/core-java/src/test/java/com/baeldung/javac/TestCompiler.java
@@ -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 compilationUnits = singletonList(new SimpleSourceFile(qualifiedClassName, testSource));
+ List 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();
+ }
+}
diff --git a/core-java/src/test/java/com/baeldung/javac/TestRunner.java b/core-java/src/test/java/com/baeldung/javac/TestRunner.java
new file mode 100644
index 0000000000..6a03ad4918
--- /dev/null
+++ b/core-java/src/test/java/com/baeldung/javac/TestRunner.java
@@ -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();
+ }
+ }
+}