diff --git a/pom.xml b/pom.xml index 8c34d98..8c779ae 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ 1.7.5 - 4.12 + 4.13.1 diff --git a/src/main/java/org/mdkt/compiler/CompilationException.java b/src/main/java/org/mdkt/compiler/CompilationException.java index 00c7f7f..feb3bcc 100644 --- a/src/main/java/org/mdkt/compiler/CompilationException.java +++ b/src/main/java/org/mdkt/compiler/CompilationException.java @@ -1,5 +1,8 @@ package org.mdkt.compiler; +/** + * 编译异常类 + */ public class CompilationException extends RuntimeException { private static final long serialVersionUID = 5272588827551900536L; diff --git a/src/main/java/org/mdkt/compiler/CompilationResult.java b/src/main/java/org/mdkt/compiler/CompilationResult.java new file mode 100644 index 0000000..e2d396f --- /dev/null +++ b/src/main/java/org/mdkt/compiler/CompilationResult.java @@ -0,0 +1,43 @@ +package org.mdkt.compiler; + +import java.util.List; +import java.util.Map; + +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; + +/** + * 编译结果收集类 + */ +public interface CompilationResult { + /** + * Return the compiled classes + */ + Map> classMap() throws ClassNotFoundException; + + /** + * Determine if the compilation did succeed + */ + boolean compilationSucceeded(); + + /** + * Determine if any warnings are present + */ + boolean hasWarnings(); + + /** + * Determine if any errors are present + */ + boolean hasErrors(); + + /** + * Return the diagnostics produced by the compiler + */ + List> getDiagnostics(); + + /** + * Throw an exception if the compilation did not succeed + */ + CompilationResult checkNoErrors(); + +} \ No newline at end of file diff --git a/src/main/java/org/mdkt/compiler/CompiledCode.java b/src/main/java/org/mdkt/compiler/CompiledCode.java index e6715cc..94b095c 100644 --- a/src/main/java/org/mdkt/compiler/CompiledCode.java +++ b/src/main/java/org/mdkt/compiler/CompiledCode.java @@ -1,5 +1,6 @@ package org.mdkt.compiler; +import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -10,11 +11,11 @@ * Created by trung on 5/3/15. */ public class CompiledCode extends SimpleJavaFileObject { - private ByteArrayOutputStream baos = new ByteArrayOutputStream(); - private String className; + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final String className; public CompiledCode(String className) throws Exception { - super(new URI(className), Kind.CLASS); + super(new URI(className), JavaFileObject.Kind.CLASS); this.className = className; } diff --git a/src/main/java/org/mdkt/compiler/DynamicClassLoader.java b/src/main/java/org/mdkt/compiler/DynamicClassLoader.java index 2128bd1..86f6848 100644 --- a/src/main/java/org/mdkt/compiler/DynamicClassLoader.java +++ b/src/main/java/org/mdkt/compiler/DynamicClassLoader.java @@ -11,6 +11,10 @@ public DynamicClassLoader(ClassLoader parent) { super(parent); } + public Map getCustomCompiledCode(){ + return this.customCompiledCode; + } + public void addCode(CompiledCode cc) { customCompiledCode.put(cc.getName(), cc); } diff --git a/src/main/java/org/mdkt/compiler/ExtendedStandardJavaFileManager.java b/src/main/java/org/mdkt/compiler/ExtendedStandardJavaFileManager.java index 1dc8a41..68806b7 100644 --- a/src/main/java/org/mdkt/compiler/ExtendedStandardJavaFileManager.java +++ b/src/main/java/org/mdkt/compiler/ExtendedStandardJavaFileManager.java @@ -1,15 +1,14 @@ package org.mdkt.compiler; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; /** * Created by trung on 5/3/15. Edited by turpid-monkey on 9/25/15, completed @@ -18,20 +17,20 @@ public class ExtendedStandardJavaFileManager extends ForwardingJavaFileManager { - private List compiledCode = new ArrayList(); - private DynamicClassLoader cl; + private final Map compiledCode; + private final DynamicClassLoader cl; /** * Creates a new instance of ForwardingJavaFileManager. * - * @param fileManager - * delegate to this file manager + * @param fileManager delegate to this file manager * @param cl */ protected ExtendedStandardJavaFileManager(JavaFileManager fileManager, - DynamicClassLoader cl) { + DynamicClassLoader cl, CompiledCode[] compiledCode) { super(fileManager); this.cl = cl; + this.compiledCode = Arrays.stream(compiledCode).collect(Collectors.toMap(CompiledCode::getClassName, c -> c)); } @Override @@ -40,8 +39,11 @@ public JavaFileObject getJavaFileForOutput( JavaFileObject.Kind kind, FileObject sibling) throws IOException { try { - CompiledCode innerClass = new CompiledCode(className); - compiledCode.add(innerClass); + CompiledCode innerClass = compiledCode.get(className); + if (innerClass == null) { + innerClass = new CompiledCode(className); + compiledCode.put(className, innerClass); + } cl.addCode(innerClass); return innerClass; } catch (Exception e) { diff --git a/src/main/java/org/mdkt/compiler/InMemoryJavaCompiler.java b/src/main/java/org/mdkt/compiler/InMemoryJavaCompiler.java index b853590..534b7f4 100644 --- a/src/main/java/org/mdkt/compiler/InMemoryJavaCompiler.java +++ b/src/main/java/org/mdkt/compiler/InMemoryJavaCompiler.java @@ -1,142 +1,215 @@ package org.mdkt.compiler; -import java.util.*; -import javax.tools.*; +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.ToolProvider; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; /** * Compile Java sources in-memory */ public class InMemoryJavaCompiler { - private JavaCompiler javac; - private DynamicClassLoader classLoader; - private Iterable options; - boolean ignoreWarnings = false; - - private Map sourceCodes = new HashMap(); - - public static InMemoryJavaCompiler newInstance() { - return new InMemoryJavaCompiler(); - } - - private InMemoryJavaCompiler() { - this.javac = ToolProvider.getSystemJavaCompiler(); - this.classLoader = new DynamicClassLoader(ClassLoader.getSystemClassLoader()); - } - - public InMemoryJavaCompiler useParentClassLoader(ClassLoader parent) { - this.classLoader = new DynamicClassLoader(parent); - return this; - } - - /** - * @return the class loader used internally by the compiler - */ - public ClassLoader getClassloader() { - return classLoader; - } - - /** - * Options used by the compiler, e.g. '-Xlint:unchecked'. - * - * @param options - * @return - */ - public InMemoryJavaCompiler useOptions(String... options) { - this.options = Arrays.asList(options); - return this; - } - - /** - * Ignore non-critical compiler output, like unchecked/unsafe operation - * warnings. - * - * @return - */ - public InMemoryJavaCompiler ignoreWarnings() { - ignoreWarnings = true; - return this; - } - - /** - * Compile all sources - * - * @return Map containing instances of all compiled classes - * @throws Exception - */ - public Map> compileAll() throws Exception { - if (sourceCodes.size() == 0) { - throw new CompilationException("No source code to compile"); - } - Collection compilationUnits = sourceCodes.values(); - CompiledCode[] code; - - code = new CompiledCode[compilationUnits.size()]; - Iterator iter = compilationUnits.iterator(); - for (int i = 0; i < code.length; i++) { - code[i] = new CompiledCode(iter.next().getClassName()); - } - DiagnosticCollector collector = new DiagnosticCollector<>(); - ExtendedStandardJavaFileManager fileManager = new ExtendedStandardJavaFileManager(javac.getStandardFileManager(null, null, null), classLoader); - JavaCompiler.CompilationTask task = javac.getTask(null, fileManager, collector, options, null, compilationUnits); - boolean result = task.call(); - if (!result || collector.getDiagnostics().size() > 0) { - StringBuffer exceptionMsg = new StringBuffer(); - exceptionMsg.append("Unable to compile the source"); - boolean hasWarnings = false; - boolean hasErrors = false; - for (Diagnostic d : collector.getDiagnostics()) { - switch (d.getKind()) { - case NOTE: - case MANDATORY_WARNING: - case WARNING: - hasWarnings = true; - break; - case OTHER: - case ERROR: - default: - hasErrors = true; - break; - } - exceptionMsg.append("\n").append("[kind=").append(d.getKind()); - exceptionMsg.append(", ").append("line=").append(d.getLineNumber()); - exceptionMsg.append(", ").append("message=").append(d.getMessage(Locale.US)).append("]"); - } - if (hasWarnings && !ignoreWarnings || hasErrors) { - throw new CompilationException(exceptionMsg.toString()); - } - } - - Map> classes = new HashMap>(); - for (String className : sourceCodes.keySet()) { - classes.put(className, classLoader.loadClass(className)); - } - return classes; - } - - /** - * Compile single source - * - * @param className - * @param sourceCode - * @return - * @throws Exception - */ - public Class compile(String className, String sourceCode) throws Exception { - return addSource(className, sourceCode).compileAll().get(className); - } - - /** - * Add source code to the compiler - * - * @param className - * @param sourceCode - * @return - * @throws Exception - * @see {@link #compileAll()} - */ - public InMemoryJavaCompiler addSource(String className, String sourceCode) throws Exception { - sourceCodes.put(className, new SourceCode(className, sourceCode)); - return this; - } + private final JavaCompiler javac; + private DynamicClassLoader classLoader; + private Iterable options; + boolean ignoreWarnings = false; + + private Map sourceCodes = new HashMap<>(); + + public static InMemoryJavaCompiler newInstance() { + return new InMemoryJavaCompiler(); + } + + private InMemoryJavaCompiler() { + this.javac = ToolProvider.getSystemJavaCompiler(); + this.classLoader = new DynamicClassLoader(ClassLoader.getSystemClassLoader()); + } + + public InMemoryJavaCompiler useParentClassLoader(ClassLoader parent) { + this.classLoader = new DynamicClassLoader(parent); + return this; + } + + /** + * @return the class loader used internally by the compiler + */ + public ClassLoader getClassloader() { + return classLoader; + } + + /** + * Options used by the compiler, e.g. '-Xlint:unchecked'. + * + * @param options + * @return + */ + public InMemoryJavaCompiler useOptions(String... options) { + this.options = Arrays.asList(options); + return this; + } + + /** + * Ignore non-critical compiler output, like unchecked/unsafe operation + * warnings. + * + * @return + */ + public InMemoryJavaCompiler ignoreWarnings() { + ignoreWarnings = true; + return this; + } + + /** + * Compile all sources + * + * @return Map containing instances of all compiled classes + * @throws Exception + */ + public Map> compileAll() throws Exception { + return compile().checkNoErrors().classMap(); + } + + /** + * Compile all sources added until now and return {@link CompilationResult}, + * providing access to the compiled classes and/or errors and warnings + */ + public CompilationResult compile() throws Exception { + if (sourceCodes.size() == 0) { + throw new CompilationException("No source code to compile"); + } + Collection compilationUnits = sourceCodes.values(); + CompiledCode[] code = new CompiledCode[compilationUnits.size()]; + Iterator iter = compilationUnits.iterator(); + for (int i = 0; i < code.length; i++) { + code[i] = new CompiledCode(iter.next().getClassName()); + } + DiagnosticCollector collector = new DiagnosticCollector<>(); + ExtendedStandardJavaFileManager fileManager = new ExtendedStandardJavaFileManager(javac.getStandardFileManager(null, null, null), classLoader, code); + JavaCompiler.CompilationTask task = javac.getTask(null, fileManager, collector, options, null, compilationUnits); + task.call(); + boolean hasWarnings; + boolean hasErrors; + + { + boolean hasWarningsTmp = false; + boolean hasErrorsTmp = false; + for (Diagnostic d : collector.getDiagnostics()) { + switch (d.getKind()) { + case NOTE: + case MANDATORY_WARNING: + case WARNING: + hasWarningsTmp = true; + break; + case OTHER: + case ERROR: + default: + hasErrorsTmp = true; + break; + } + } + hasWarnings = hasWarningsTmp; + hasErrors = hasErrorsTmp; + } + + return new CompilationResult() { + + @Override + public Map> classMap() throws ClassNotFoundException { + Map> classes = new HashMap<>(); + for (String className : sourceCodes.keySet()) { + // 由loadClass变更为findClass,优先获取新编译的同名类(如果使用loadClass则获取不到新编译的同名class) + // 虽然支持多次加载同名类(指不同的ClassLoader,实际上同一个ClassLoader中Java仍然不允许重复会报错) + // 但是还是不推荐这样做,因为这样违反了Java的基本原则,且容易出现混淆影响自己问题的处理 +// classes.put(className, classLoader.loadClass(className)); + classes.put(className, classLoader.findClass(className)); + } + return classes; + } + + @Override + public boolean compilationSucceeded() { + if (hasWarnings && !ignoreWarnings) return false; + return !hasErrors; + } + + @Override + public boolean hasWarnings() { + return hasWarnings; + } + + @Override + public boolean hasErrors() { + return hasErrors; + } + + @Override + public List> getDiagnostics() { + return collector.getDiagnostics(); + } + + @Override + public CompilationResult checkNoErrors() { + if (!compilationSucceeded()) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Unable to compile the source"); + for (Diagnostic d : getDiagnostics()) { + exceptionMsg.append("\n").append("[kind=").append(d.getKind()); + exceptionMsg.append(", ").append("line=").append(d.getLineNumber()); + exceptionMsg.append(", ").append("message=").append(d.getMessage(Locale.US)).append("]"); + } + throw new CompilationException(exceptionMsg.toString()); + } + return this; + } + + }; + } + + + /** + * Compile single source + * + * @param className + * @param sourceCode + * @return + * @throws Exception + */ + public Class compile(String className, String sourceCode) throws Exception { + return addSource(className, sourceCode).compileAll().get(className); + } + + /** + * Add source code to the compiler + * + * @param className + * @param sourceCode + * @return + * @throws Exception + * @see {@link #compileAll()} + */ + public InMemoryJavaCompiler addSource(String className, String sourceCode) throws Exception { + sourceCodes.put(className, new SourceCode(className, sourceCode)); + return this; + } + + /** + * Add source code map to the compiler + * + * @param sourceCodeMap + * @return + * @throws Exception + */ + public InMemoryJavaCompiler addSources(Map sourceCodeMap) throws Exception { + sourceCodes.putAll(sourceCodeMap); + return this; + } } diff --git a/src/main/java/org/mdkt/compiler/SourceCode.java b/src/main/java/org/mdkt/compiler/SourceCode.java index 9c7a9e7..acc9d49 100644 --- a/src/main/java/org/mdkt/compiler/SourceCode.java +++ b/src/main/java/org/mdkt/compiler/SourceCode.java @@ -1,5 +1,6 @@ package org.mdkt.compiler; +import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import java.io.IOException; import java.net.URI; @@ -9,11 +10,11 @@ */ public class SourceCode extends SimpleJavaFileObject { private String contents = null; - private String className; + private final String className; public SourceCode(String className, String contents) throws Exception { super(URI.create("string:///" + className.replace('.', '/') - + Kind.SOURCE.extension), Kind.SOURCE); + + JavaFileObject.Kind.SOURCE.extension), JavaFileObject.Kind.SOURCE); this.contents = contents; this.className = className; } @@ -22,6 +23,7 @@ public String getClassName() { return className; } + @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return contents; diff --git a/src/test/java/org/mdkt/compiler/HelloClass.java b/src/test/java/org/mdkt/compiler/HelloClass.java new file mode 100644 index 0000000..7694cc7 --- /dev/null +++ b/src/test/java/org/mdkt/compiler/HelloClass.java @@ -0,0 +1,8 @@ +package org.mdkt.compiler; + +public class HelloClass { + + public String hello() { + return "hello"; + } +} \ No newline at end of file diff --git a/src/test/java/org/mdkt/compiler/InMemoryJavaCompilerTest.java b/src/test/java/org/mdkt/compiler/InMemoryJavaCompilerTest.java index c3c16e8..2e1b02a 100644 --- a/src/test/java/org/mdkt/compiler/InMemoryJavaCompilerTest.java +++ b/src/test/java/org/mdkt/compiler/InMemoryJavaCompilerTest.java @@ -1,8 +1,5 @@ package org.mdkt.compiler; -import java.util.List; -import java.util.Map; - import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -10,6 +7,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.Map; + public class InMemoryJavaCompilerTest { private static final Logger logger = LoggerFactory.getLogger(InMemoryJavaCompilerTest.class); @@ -18,7 +18,7 @@ public class InMemoryJavaCompilerTest { @Test public void compile_WhenTypical() throws Exception { - StringBuffer sourceCode = new StringBuffer(); + StringBuilder sourceCode = new StringBuilder(); sourceCode.append("package org.mdkt;\n"); sourceCode.append("public class HelloClass {\n"); @@ -30,19 +30,50 @@ public void compile_WhenTypical() throws Exception { Assert.assertEquals(1, helloClass.getDeclaredMethods().length); } + @Test + public void compile_WhenTypicalUpdateClass() throws Exception { + StringBuilder sourceCode = new StringBuilder(); + + sourceCode.append("package org.mdkt.compiler;\n"); + sourceCode.append("public class HelloClass {\n"); + sourceCode.append(" public String hello() { return \"hello1\"; }"); + sourceCode.append("}"); + + Class oldClass = HelloClass.class; + Class newClass = InMemoryJavaCompiler.newInstance().compile("org.mdkt.compiler.HelloClass", sourceCode.toString()); + + Assert.assertNotEquals(oldClass.hashCode() , newClass.hashCode()); + Assert.assertNotEquals(oldClass.getDeclaredMethod("hello").invoke(oldClass.newInstance()) , + newClass.getDeclaredMethod("hello").invoke(newClass.newInstance())); + } + @Test public void compileAll_WhenTypical() throws Exception { String cls1 = "public class A{ public B b() { return new B(); }}"; String cls2 = "public class B{ public String toString() { return \"B!\"; }}"; + String cls3 = "import org.mdkt.compiler.ShanhyTest; public class C{ public String hello() { return new ShanhyTest().hello(); }}"; - Map> compiled = InMemoryJavaCompiler.newInstance().addSource("A", cls1).addSource("B", cls2).compileAll(); + Map> compiled = InMemoryJavaCompiler.newInstance() + .addSource("A", cls1) +// .addSource("A", cls1) +// .addSource("A", cls1) + .addSource("B", cls2) + .addSource("C", cls3) + .compileAll(); Assert.assertNotNull(compiled.get("A")); Assert.assertNotNull(compiled.get("B")); + Assert.assertNotNull(compiled.get("C")); Class aClass = compiled.get("A"); Object a = aClass.newInstance(); Assert.assertEquals("B!", aClass.getMethod("b").invoke(a).toString()); + + Class cClass = compiled.get("C"); + Object c = cClass.newInstance(); + String helloInvokeResult = cClass.getMethod("hello").invoke(c).toString(); + System.out.println("helloInvokeResult >>> " + helloInvokeResult); + Assert.assertEquals("Hello Shanhy", helloInvokeResult); } @Test @@ -87,21 +118,21 @@ public void compile_WhenFailOnWarnings() throws Exception { @Test public void compile_WhenIgnoreWarnings() throws Exception { - StringBuffer sourceCode = new StringBuffer(); + StringBuilder sourceCode = new StringBuilder(); sourceCode.append("package org.mdkt;\n"); sourceCode.append("public class HelloClass {\n"); sourceCode.append(" public java.util.List hello() { return new java.util.ArrayList(); }"); sourceCode.append("}"); Class helloClass = InMemoryJavaCompiler.newInstance().ignoreWarnings().compile("org.mdkt.HelloClass", sourceCode.toString()); - List res = (List) helloClass.getMethod("hello").invoke(helloClass.newInstance()); + List res = (List) helloClass.getMethod("hello").invoke(helloClass.getDeclaredConstructor().newInstance()); Assert.assertEquals(0, res.size()); } @Test public void compile_WhenWarningsAndErrors() throws Exception { thrown.expect(CompilationException.class); - StringBuffer sourceCode = new StringBuffer(); + StringBuilder sourceCode = new StringBuilder(); sourceCode.append("package org.mdkt;\n"); sourceCode.append("public class HelloClass extends xxx {\n"); @@ -114,4 +145,5 @@ public void compile_WhenWarningsAndErrors() throws Exception { throw e; } } + } diff --git a/src/test/java/org/mdkt/compiler/ShanhyTest.java b/src/test/java/org/mdkt/compiler/ShanhyTest.java new file mode 100644 index 0000000..2b1acd9 --- /dev/null +++ b/src/test/java/org/mdkt/compiler/ShanhyTest.java @@ -0,0 +1,13 @@ +package org.mdkt.compiler; + +/** + * @author shanhy + * @date 2023-06-05 15:43 + */ +public class ShanhyTest { + + public String hello(){ + return "Hello Shanhy"; + } + +}