Иногда требуется создать класс с определенным поведением во время работы программы. К сожалению, Java не обладает динамизмом, присущем таким языками как Python, Javascript, Groovy. Но все же, возможность такая существует, и есть несколько способов это сделать. Например, сгенерировать напрямую байткод. Но такой способ, довольно сложен. Мы же, в данной статье, напишем исходный код и затем скомпилируем его.
Для компиляции в стандартной библиотеке Java, существует класс JavaCompiler
. Следующий метод производит компиляцию исходного кода и возвращает путь до директории со компилированными классами:
private static String compile(String className, String code) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Path output = Files.createTempDirectory("_" + System.currentTimeMillis());
JavaCompiler.CompilationTask task = compiler
.getTask(null,
fileManager,
null,
Arrays.asList("-d", output.toAbsolutePath().toString()),
null,
singletonList(new JavaSourceFromString(className, code))
);
boolean result = task.call();
if(!result) throw new IllegalStateException("something wrong happened");
return output.toAbsolutePath().toString();
}
Метод getTask
последним параметром принимает, коллекцию объектов. Эти объекты являются представлением некоторого источника исходного кода. Это может быть либо файл, либо как в нашем примере, строка. Готового класса, для строки не нашлось, поэтому приходится создать его самостоятельно:
private static class JavaSourceFromString extends SimpleJavaFileObject {
private final String code;
private JavaSourceFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
Чтобы загрузить полученный класс, нам потребуется ClassLoader
. Создать его можно следующим образом:
private static ClassLoader getClassLoader(String classPath) throws Exception {
return new URLClassLoader(
new URL[]{Paths
.get(classPath)
.toUri()
.toURL()
},
null);
}
Теперь имея ClassLoader
, мы можем получить объект типа Class<?>
и создать экземпляр нужного класса. Но, т.к. класс во время компиляции не был известен, соотвественно, напрямую вызывать какие либо методы из него мы не можем (кроме тех, что объявлены в классе java.lang.Object
). Поэтому нужно использовать механизм Reflection.
private static void method(Class<?> type, String methodName) throws Exception {
Method method = type.getDeclaredMethod(methodName);
method.setAccessible(true);
System.out.println(method.invoke(type.newInstance()));
}
Следующий код, создает два класса ru.izebit.Person
, и вызывает у них метод say
:
ClassLoader classLoader = CustomCompiler.getClassLoader("ru.izebit.Person",
"package ru.izebit;\n" +
"public class Person {\n" +
" public String say() {\n" +
" return \"i am happy 🤠\";\n" +
" }\n" +
"}");
method(classLoader.loadClass("ru.izebit.Person"), "say");
classLoader = CustomCompiler.getClassLoader("ru.izebit.Person",
"package ru.izebit;\n" +
"public class Person {\n" +
" public String say() {\n" +
" return \"i am tired 😫 \";\n" +
" }\n" +
"}");
method(classLoader.loadClass("ru.izebit.Person"), "say");
Заметьте, что классы имеют одинаковые названия, с точностью до пакета. Но т.к. используются разные ClassLoader
‘ы, то это вполне допустимо и никакого конфликта нет. Результат работы, как вы уже догадались будет следующим:
i am happy 🤠
i am tired 😫
Весь код целиком можно посмотреть на github.