initial commit
This commit is contained in:
commit
3071220d19
|
|
@ -0,0 +1,39 @@
|
||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
|
/.idea/
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
### Usage example
|
||||||
|
```java
|
||||||
|
var compiler = new RuntimeCompiler();
|
||||||
|
var classname = "com.Test.App";
|
||||||
|
var source = """
|
||||||
|
package com.Test;
|
||||||
|
public class App implements Runnable {
|
||||||
|
public void run() {
|
||||||
|
System.out.println("It works! ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
var compiled = compiler.compile(new SourceCode(classname, source));
|
||||||
|
var constructor = compiled.getConstructor();
|
||||||
|
var instance = (Runnable) constructor.newInstance();
|
||||||
|
instance.run();
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>TestRuntimeCompiler</artifactId>
|
||||||
|
<version>1.0.0.0</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<version>5.11.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<version>5.11.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/org.easytesting/fest-assert-core -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.easytesting</groupId>
|
||||||
|
<artifactId>fest-assert-core</artifactId>
|
||||||
|
<version>2.0M10</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package ru.kirillius.compiler;
|
||||||
|
|
||||||
|
import javax.tools.DiagnosticCollector;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
|
public class CompilationException extends Exception {
|
||||||
|
public CompilationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompilationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompilationException build(DiagnosticCollector<?> diagnostics) {
|
||||||
|
var message = new StringJoiner("\n");
|
||||||
|
diagnostics.getDiagnostics().forEach(diagnostic -> message.add(diagnostic.toString()));
|
||||||
|
return new CompilationException(message.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package ru.kirillius.compiler;
|
||||||
|
|
||||||
|
import javax.tools.SimpleJavaFileObject;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public class CompiledFileBytecode extends SimpleJavaFileObject {
|
||||||
|
|
||||||
|
protected ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
public CompiledFileBytecode(String name, Kind kind) {
|
||||||
|
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] asByteArray() {
|
||||||
|
return bos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream openOutputStream() {
|
||||||
|
return bos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package ru.kirillius.compiler;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
public class RuntimeClassLoader extends ClassLoader {
|
||||||
|
|
||||||
|
private final RuntimeFileManager manager;
|
||||||
|
|
||||||
|
public RuntimeClassLoader(ClassLoader parent, RuntimeFileManager manager) {
|
||||||
|
super(parent);
|
||||||
|
this.manager = requireNonNull(manager, "manager must not be null");
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
|
var compiledClasses = manager.getClasses();
|
||||||
|
if (compiledClasses.containsKey(name)) {
|
||||||
|
var bytes = compiledClasses.get(name).asByteArray();
|
||||||
|
return defineClass(name, bytes, 0, bytes.length);
|
||||||
|
} else {
|
||||||
|
throw new ClassNotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package ru.kirillius.compiler;
|
||||||
|
|
||||||
|
|
||||||
|
import javax.tools.DiagnosticCollector;
|
||||||
|
import javax.tools.JavaCompiler;
|
||||||
|
import javax.tools.ToolProvider;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class RuntimeCompiler {
|
||||||
|
|
||||||
|
private final RuntimeFileManager fileManager;
|
||||||
|
private final JavaCompiler compiler;
|
||||||
|
|
||||||
|
public RuntimeCompiler() {
|
||||||
|
compiler = ToolProvider.getSystemJavaCompiler();
|
||||||
|
if (compiler == null) {
|
||||||
|
throw new RuntimeException("System Java compiler is not available");
|
||||||
|
}
|
||||||
|
fileManager = new RuntimeFileManager(compiler.getStandardFileManager(null, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<?> compile(SourceCode sourceCode) throws CompilationException {
|
||||||
|
var diagnostics = new DiagnosticCollector<>();
|
||||||
|
var task = compiler.getTask(null, fileManager, diagnostics, null, null, Collections.singletonList(sourceCode));
|
||||||
|
if (!task.call()) {
|
||||||
|
throw CompilationException.build(diagnostics);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return fileManager.getClassLoader(null).loadClass(sourceCode.getClassName());
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
throw new CompilationException("Unable to load compiled class", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ru.kirillius.compiler;
|
||||||
|
|
||||||
|
import javax.tools.*;
|
||||||
|
import java.util.Hashtable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class RuntimeFileManager extends ForwardingJavaFileManager<JavaFileManager> {
|
||||||
|
public RuntimeFileManager(StandardJavaFileManager standardManager) {
|
||||||
|
super(standardManager);
|
||||||
|
classes = new Hashtable<>();
|
||||||
|
loader = new RuntimeClassLoader(this.getClass().getClassLoader(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ClassLoader loader;
|
||||||
|
private final Map<String, CompiledFileBytecode> classes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassLoader getClassLoader(Location location) {
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) {
|
||||||
|
var bytecode = new CompiledFileBytecode(className, kind);
|
||||||
|
classes.put(className, bytecode);
|
||||||
|
return bytecode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, CompiledFileBytecode> getClasses() {
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ru.kirillius.compiler;
|
||||||
|
|
||||||
|
import javax.tools.SimpleJavaFileObject;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
public class SourceCode extends SimpleJavaFileObject {
|
||||||
|
|
||||||
|
private final String sourceCode;
|
||||||
|
private final String className;
|
||||||
|
|
||||||
|
public SourceCode(String classname, String sourceCode) {
|
||||||
|
super(URI.create("string:///" + classname.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
|
||||||
|
this.sourceCode = requireNonNull(sourceCode, "sourceCode must not be null");
|
||||||
|
this.className = classname;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
|
||||||
|
return sourceCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClassName() {
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package ru.kirillius.compiler;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class RuntimeCompilerTest {
|
||||||
|
|
||||||
|
private <T> T compile(SourceCode sourceCode, Class<T> type) {
|
||||||
|
var compiler = new RuntimeCompiler();
|
||||||
|
try {
|
||||||
|
var compiled = compiler.compile(sourceCode);
|
||||||
|
assertThat(compiled).isNotNull();
|
||||||
|
var constructor = compiled.getConstructor();
|
||||||
|
return (T) constructor.newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestBasicCompilation() {
|
||||||
|
var classname = "com.Test.App";
|
||||||
|
var source = """
|
||||||
|
package com.Test;
|
||||||
|
import java.util.function.Function;
|
||||||
|
public class App implements Function<Integer, Integer> {
|
||||||
|
@Override
|
||||||
|
public Integer apply(Integer i) {
|
||||||
|
return i*2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var function = (Function<Integer, Integer>) compile(new SourceCode(classname, source), Function.class);
|
||||||
|
assertThat(function.apply(1)).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestMultipleCompilation() {
|
||||||
|
|
||||||
|
for (var i = 0; i < 30; i++) {
|
||||||
|
var number = (int) (Math.random() * 1000);
|
||||||
|
var source = createNewSource(number);
|
||||||
|
var function = (Function<Integer, Integer>) compile(source, Function.class);
|
||||||
|
assertThat(function.apply(i)).isEqualTo(number + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void CompileBrokenCode() {
|
||||||
|
var compiler = new RuntimeCompiler();
|
||||||
|
try {
|
||||||
|
compiler.compile(new SourceCode("broken1", "this code is broken"));
|
||||||
|
throw new RuntimeException();
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e).isInstanceOf(CompilationException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void CompileInvalidClassname() {
|
||||||
|
var compiler = new RuntimeCompiler();
|
||||||
|
try {
|
||||||
|
compiler.compile(new SourceCode("broken2", "public class NotBroken{ public int x; }"));
|
||||||
|
throw new RuntimeException();
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e).isInstanceOf(CompilationException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SourceCode createNewSource(int magicNumber) {
|
||||||
|
var classname = "com.Test.App" + magicNumber;
|
||||||
|
var source = """
|
||||||
|
package com.Test;
|
||||||
|
import java.util.function.Function;
|
||||||
|
public class App""" + magicNumber + """
|
||||||
|
implements Function<Integer, Integer> {
|
||||||
|
@Override
|
||||||
|
public Integer apply(Integer i) {
|
||||||
|
return i + """ + magicNumber + """
|
||||||
|
; }
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
return new SourceCode(classname, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue