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