Class.forName (name, instantiation, classLoader) не добавляет класс в путь к классам - PullRequest
0 голосов
/ 21 мая 2019

Я генерирую .java файлы классов во время выполнения и должен немедленно использовать эти классы внутри кода. Поэтому я компилирую классы .java, используя API компилятора для создания .class файлов:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

StandardJavaFileManager manager = compiler.getStandardFileManager(diagnostics, null, null);

File file = new File("path to file");

Iterable<? extends JavaFileObject> sources = manager.getJavaFileObjectsFromFiles(Arrays.asList(file));

CompilationTask task = compiler.getTask(null, manager, diagnostics, null, null, sources);

task.call();

manager.close();

Затем мне нужно получить ссылки на эти скомпилированные классы, используя Class.forName(), но если я просто вызову Class.forName("com.foo.Bar"), то получится ClassNotFoundException, при условии, что новые файлы .class не добавлены в classpath I искал методы добавления классов к classpath во время выполнения. Я столкнулся с некоторыми неясностями, связанными с этим понятием:

1. Правильный ли этот подход (сначала скомпилировать файл .java, использовать API компилятора и добавить его в загрузчик классов на втором шаге)? Чтобы можно было мгновенно использовать класс в коде.

2. AFAIK, Есть 2 метода для динамической загрузки классов в classpath во время выполнения: один использует пользовательский ClassLoader, как это: (который я скомпилировал, так как он жаловался, что BuiltinClassLoader не addURL метод):

    // Get the ClassLoader class
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    Class<?> clazz = cl.getClass();

    // Get the protected addURL method from the parent URLClassLoader class
    Method method = clazz.getSuperclass().getDeclaredMethod("addURL", new Class[] { URL.class });

    // Run projected addURL method to add JAR to classpath
    method.setAccessible(true);
    method.invoke(cl, new Object[] { cls });

Другой метод использует Class.forName(name, instantiation, classLoader) для добавления класса в classpath (который также дает ссылку на класс). Первый метод, который я не смог применить, так как я получил ошибку компилятора (Java 11), как упомянуто выше. Что касается второго метода, присоединит ли Class.forName(name, instantiation, classLoader) новые классы к classpath, если мы вызовем загрузчик классов по умолчанию, как этот? :

Class.forName("com.foo.Bar",true, ClassLoader.getSystemClassLoader());
// or:
Class.forName("com.foo.Bar",true, ApiHandler.class.getClassLoader());

Это не работает для меня. Какой вариант приведенных выше аргументов classLoader верны и почему они не работают? Обязательно ли создавать собственный загрузчик классов и передавать его в Class.forName()?

3. Я создаю .java файлы в пакете com.foo в папке src проекта eclipse. Их скомпилированные .class файлы также генерируются в той же папке (с использованием API компилятора). Когда я обновляю проект, используя eclipse (щелкнув правой кнопкой мыши по проекту -> Обновить), соответствующие файлы .class будут сгенерированы в папке target/classes, и тогда к классам можно будет получить доступ через код (например, с помощью Class.forName("com.foo.Bar)). Может быть, если я создам .class файлов (с помощью API компилятора) в папке target/classes, классы будут распознаваемы без необходимости их введения в путь к классам?


UPDATE:

Я смог использовать скомпилированные классы в своем коде, сохранив уважаемые файлы .class в папке target/classes, упомянутой в третьем вопросе выше) проекта. (Путем добавления опции -d к методу getTask() компилятора:

Iterable<String> options = Arrays.asList( new String[] { "-d", System.getProperty("user.dir") + "/target/classes/"} );
.
.
.

CompilationTask task = compiler.getTask(null, manager, diagnostics, options, null, sources);

Таким образом, кажется, что классы даже не нужно добавлять в путь к классам с помощью classLoader ; так как класс доступен с помощью простого Class.forName(). Как вы это объясните?

Class<?> cls1 = Class.forName("com.foo.Bar");

А также через ClassLoader, конечно:

ClassLoader classLoader = ClassLoader.getSystemClassLoader(); 

Class<?> cls = classLoader.loadClass("com.foo.Bar");

1 Ответ

0 голосов
/ 05 июня 2019

Самое безопасное решение - создать новую реализацию ClassLoader и загрузить сгенерированные классы через новый загрузчик, как показано в этот ответ .

Но, начиная с Java 9, существует возможность определять классы в вашем собственном контексте, то есть в том же пакете, если еще не определен / не загружен класс с таким именем. Такое определение класса может даже заменить определение пути к классу, как уже было сказано, если оно еще не загружено. Таким образом, не только последующие Class.forName(String) вызовы будут разрешены к этому определению класса, но даже неотражающие ссылки.

Это можно продемонстрировать с помощью следующей программы.

class Dummy { // to make the compiler happy
    static native void extensionMethod();
}
public class CompileExtension {
    public static void main(String[] args) throws IOException, IllegalAccessException {
        // customize these, if you want, null triggers default behavior
        DiagnosticListener<JavaFileObject> diagnosticListener = null;
        Locale locale = null;

        // the actual class implementation, to be present at runtime only
        String class1 =
            "class Dummy {\n"
          + "    static void extensionMethod() {\n"
          + "        System.out.println(\"hello from dynamically compiled code\");\n"
          + "    }\n"
          + "}";
        JavaCompiler c = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fm
          = c.getStandardFileManager(diagnosticListener, locale, Charset.defaultCharset());
        // define where to store compiled class files - use a temporary directory
        fm.setLocation(StandardLocation.CLASS_OUTPUT,
            Set.of(Files.createTempDirectory("compile-test").toFile()));
        JavaCompiler.CompilationTask task = c.getTask(null, fm,
            diagnosticListener, Set.of(), Set.of(),
            Set.of(new SimpleJavaFileObject(
                URI.create("string:///Class1.java"), JavaFileObject.Kind.SOURCE) {
                    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                        return class1;
                    }
                }));

        if(task.call()) {
            FileObject fo = fm.getJavaFileForInput(
                StandardLocation.CLASS_OUTPUT, "Dummy", JavaFileObject.Kind.CLASS);
            // these are the class bytes of the first class
            byte[] classBytes = Files.readAllBytes(Paths.get(fo.toUri()));
            MethodHandles.lookup().defineClass(classBytes);

            Dummy.extensionMethod();
        }
    }
}

Определение Dummy существует только для возможности вставки вызова в нужный метод во время компиляции, тогда как во время выполнения динамически определенный класс занимает свое место до вызова метода.

Но будь осторожен. Как уже было сказано, пользовательский загрузчик классов является самым безопасным решением. Обычно вы должны создавать ссылки во время компиляции на расширения через интерфейс, который всегда присутствует, и динамически загружать только реализации, которые можно привести к интерфейсу во время выполнения и затем использовать через API, определенный интерфейсом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...