Иногда требуется создать класс с определенным поведением во время работы программы. К сожалению, 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.