GroovyScriptEngine создает исключение MultipleCompilationErrorsException при загрузке класса, который использует статический внутренний класс другого класса - PullRequest
0 голосов
/ 12 сентября 2018

У меня проблема с GroovyScriptEngine - похоже, он не может работать с внутренними классами.Кто-нибудь знает, есть ли какое-то ограничение в GroovyScriptEngine или обходной путь?

У меня есть каталог с этими двумя файлами:

// MyClass.groovy

public class MyClass {
    MyOuter m1;
    MyOuter.MyInner m2;
}

и

// MyOuter.groovy

public class MyOuter {
    public static class MyInner {}
}

У меня есть следующиетестовый класс:

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

import groovy.util.GroovyScriptEngine;

public class TestGroovyScriptEngine {

    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {

        final File myGroovySourceDir = new File("C:/MyGroovySourceDir");

        final URL[] urls = { myGroovySourceDir.toURL() };
        GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine(urls,
                Thread.currentThread().getContextClassLoader());

        Class<?> clazz = groovyScriptEngine.getGroovyClassLoader().loadClass("MyClass");
    }

}

Когда я запускаю его, я получаю следующую ошибку компиляции:

Exception in thread "main" org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
C:\MyGroovySourceDir\MyClass.groovy: 3: unable to resolve class MyOuter.MyInner 
 @ line 3, column 2.
    MyOuter.MyInner m2;
    ^

1 error

    at org.codehaus.groovy.control.ErrorCollector.failIfErrors(ErrorCollector.java:311)
    at org.codehaus.groovy.control.CompilationUnit.applyToSourceUnits(CompilationUnit.java:983)
    at org.codehaus.groovy.control.CompilationUnit.doPhaseOperation(CompilationUnit.java:633)
    at org.codehaus.groovy.control.CompilationUnit.compile(CompilationUnit.java:582)
    at groovy.lang.GroovyClassLoader.doParseClass(GroovyClassLoader.java:354)
    at groovy.lang.GroovyClassLoader.access$300(GroovyClassLoader.java:87)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:323)
    at groovy.lang.GroovyClassLoader$5.provide(GroovyClassLoader.java:320)
    at org.codehaus.groovy.runtime.memoize.ConcurrentCommonCache.getAndPut(ConcurrentCommonCache.java:147)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:318)
    at groovy.util.GroovyScriptEngine$ScriptClassLoader.doParseClass(GroovyScriptEngine.java:248)
    at groovy.util.GroovyScriptEngine$ScriptClassLoader.parseClass(GroovyScriptEngine.java:235)
    at groovy.lang.GroovyClassLoader.parseClass(GroovyClassLoader.java:307)
    at groovy.lang.GroovyClassLoader.recompile(GroovyClassLoader.java:811)
    at groovy.lang.GroovyClassLoader.loadClass(GroovyClassLoader.java:767)
    at groovy.lang.GroovyClassLoader.loadClass(GroovyClassLoader.java:836)
    at groovy.lang.GroovyClassLoader.loadClass(GroovyClassLoader.java:824)

Я бы ожидал "чистой компиляции", но внутренний класс, кажется, вызываетпроблемы.

Мои классы Groovy прекрасно компилируются в командной строке с помощью groovyc или в Eclipse.

1 Ответ

0 голосов
/ 13 сентября 2018

Вы столкнулись с крайним случаем здесь.Чтобы выяснить, что происходит, давайте определим начальные условия:

  • у вас есть класс Java (или Groovy), который выполняется внутри JVM
  • , у вас есть два класса Groovy, которые загружаются внеJVM

Описанная вами проблема не существует, если вы поместите эти два класса Groovy в тот же путь, по которому вы выполняете свой класс Java, - в этом случае IDE заботится о компиляции этих классов Groovy и их размещениик пути к классу JVM, который начинает запускать ваш тестовый класс Java.

Но это не ваш случай, и вы пытаетесь загрузить эти два класса Groovy вне работающей JVM, используя GroovyClassLoader (что расширяет URLClassLoader кстати).Я постараюсь объяснить простейшими словами, что случилось, что при добавлении поля типа MyOuter не возникает ошибка компиляции, но MyOuter.MyInner.

Когда вы выполняете:

Class<?> clazz = groovyScriptEngine.getGroovyClassLoader().loadClass("MyClass");

Загрузчик классов Groovy переходит к части поиска файла сценария, поскольку он не смог найти MyClass в текущем пути к классам.Это часть, ответственная за это:

    // at this point the loading from a parent loader failed
    // and we want to recompile if needed.
    if (lookupScriptFiles) {
        // try groovy file
        try {
            // check if recompilation already happened.
            final Class classCacheEntry = getClassCacheEntry(name);
            if (classCacheEntry != cls) return classCacheEntry;
            URL source = resourceLoader.loadGroovySource(name);
            // if recompilation fails, we want cls==null
            Class oldClass = cls;
            cls = null;
            cls = recompile(source, name, oldClass);
        } catch (IOException ioe) {
            last = new ClassNotFoundException("IOException while opening groovy source: " + name, ioe);
        } finally {
            if (cls == null) {
                removeClassCacheEntry(name);
            } else {
                setClassCacheEntry(cls);
            }
        }
    }

Источник: src / main / groovy / lang / GroovyClassLoader.java # L733-L753

Здесь URL source = resourceLoader.loadGroovySource(name); загружает полный URL-адрес файла в исходный файл, а здесь cls = recompile(source, name, oldClass); выполняет компиляцию класса.

Есть несколько фаз , вовлеченных в компиляцию класса Groovy.Одним из них является Phase.SEMANTIC_ANALYSIS, который, например, анализирует поля класса и их типы.В этот момент ClassCodeVisitorSupport выполняет visitClass(ClassNode node) для MyClass класса, а следующая строка

node.visitContents(this);

запускает обработку содержимого класса.Если мы посмотрим на исходный код этого метода:

public void visitContents(GroovyClassVisitor visitor) {
    // now let's visit the contents of the class
    for (PropertyNode pn : getProperties()) {
        visitor.visitProperty(pn);
    }

    for (FieldNode fn : getFields()) {
        visitor.visitField(fn);
    }

    for (ConstructorNode cn : getDeclaredConstructors()) {
        visitor.visitConstructor(cn);
    }

    for (MethodNode mn : getMethods()) {
        visitor.visitMethod(mn);
    }
}

Источник: src / main / org / codehaus / groovy / ast / ClassNode.java # L1066-L108

мы увидим, что он анализирует и обрабатывает свойства класса, поля, конструкторы и методы.На этом этапе он разрешает все типы, определенные для этих элементов.Он видит, что есть два свойства m1 и m2 с типами MyOuter и MyOuter.MyInner соответственно, и выполняет для них visitor.visitProperty(pn);.Этот метод выполняет тот, который мы ищем - resolve()

private boolean resolve(ClassNode type, boolean testModuleImports, boolean testDefaultImports, boolean testStaticInnerClasses) {
    resolveGenericsTypes(type.getGenericsTypes());
    if (type.isResolved() || type.isPrimaryClassNode()) return true;
    if (type.isArray()) {
        ClassNode element = type.getComponentType();
        boolean resolved = resolve(element, testModuleImports, testDefaultImports, testStaticInnerClasses);
        if (resolved) {
            ClassNode cn = element.makeArray();
            type.setRedirect(cn);
        }
        return resolved;
    }

    // test if vanilla name is current class name
    if (currentClass == type) return true;

    String typeName = type.getName();

    if (genericParameterNames.get(typeName) != null) {
        GenericsType gt = genericParameterNames.get(typeName);
        type.setRedirect(gt.getType());
        type.setGenericsTypes(new GenericsType[]{ gt });
        type.setGenericsPlaceHolder(true);
        return true;
    }

    if (currentClass.getNameWithoutPackage().equals(typeName)) {
        type.setRedirect(currentClass);
        return true;
    }

    return resolveNestedClass(type) ||
            resolveFromModule(type, testModuleImports) ||
            resolveFromCompileUnit(type) ||
            resolveFromDefaultImports(type, testDefaultImports) ||
            resolveFromStaticInnerClasses(type, testStaticInnerClasses) ||
            resolveToOuter(type);
}

Источник: src / main / org / codehaus / groovy / control / ResolveVisitor.java # L343-L378

Этот метод выполняется для классов MyOuter и MyOuter.MyInner.Стоит отметить, что механизм разрешения классов только проверяет, доступен ли данный класс в пути к классам, и не загружает или не анализирует какие-либо классы.Вот почему MyOuter распознается, когда этот метод достигает resolveToOuter(type).Если мы взглянем на его исходный код, то поймем, почему он работает для этого класса:

private boolean resolveToOuter(ClassNode type) {
    String name = type.getName();

    // We do not need to check instances of LowerCaseClass
    // to be a Class, because unless there was an import for
    // for this we do not lookup these cases. This was a decision
    // made on the mailing list. To ensure we will not visit this
    // method again we set a NO_CLASS for this name
    if (type instanceof LowerCaseClass) {
        classNodeResolver.cacheClass(name, ClassNodeResolver.NO_CLASS);
        return false;
    }

    if (currentClass.getModule().hasPackageName() && name.indexOf('.') == -1) return false;
    LookupResult lr = null;
    lr = classNodeResolver.resolveName(name, compilationUnit);
    if (lr!=null) {
        if (lr.isSourceUnit()) {
            SourceUnit su = lr.getSourceUnit();
            currentClass.getCompileUnit().addClassNodeToCompile(type, su);
        } else {
            type.setRedirect(lr.getClassNode());
        }
        return true;
    }
    return false;
}

Источник: src / main / org / codehaus / groovy / control / ResolveVisitor.java # L725-L751

Когда загрузчик классов Groovy пытается разрешить MyOuter имя типа, он достигает

lr = classNodeResolver.resolveName(name, compilationUnit);

, который находит скрипт с именем MyOuter.groovyи он создает объект SourceUnit, связанный с этим именем файла сценария.Это просто что-то вроде высказывания "ОК, этот класс в данный момент отсутствует в моем classpath, но есть исходный файл, который я вижу, что после компиляции он предоставит действительный тип имени MyOuter" ,Вот почему он наконец достигает:

currentClass.getCompileUnit().addClassNodeToCompile(type, su);

, где currentClass - это объект, связанный с типом MyClass - он добавляет этот исходный модуль к MyClass модулю компиляции, поэтому он компилируется с MyClass учебный класс.И на этом заканчивается разрешение свойства класса

MyOuter m1

.

На следующем шаге он выбирает свойство MyOuter.MyInner m2 и пытается разрешить его тип.Имейте в виду - MyOuter было решено правильно, но оно не было загружено в путь к классам, поэтому его статический внутренний класс еще не существует ни в одной области видимости.Он использует те же стратегии разрешения, что и MyOuter, но любая из них работает для класса MyOuter.MyInner.И именно поэтому ResolveVisitor.resolveOrFail() в конечном итоге выдает это исключение компиляции.

Обходной путь

Хорошо, мы знаем, что происходит, но можем ли мы что-нибудь с этим сделать?К счастью, есть решение этой проблемы.Вы можете запустить вашу программу и успешно загрузить MyClass, только если вы сначала загрузите класс MyOuter в обработчик сценариев Groovy:

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;

import groovy.util.GroovyScriptEngine;

public class TestGroovyScriptEngine {

    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {

        final File myGroovySourceDir = new File("C:/MyGroovySourceDir");

        final URL[] urls = { myGroovySourceDir.toURL() };
        GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine(urls,
                Thread.currentThread().getContextClassLoader());

        groovyScriptEngine.getGroovyClassLoader().loadClass("MyOuter");

        Class<?> clazz = groovyScriptEngine.getGroovyClassLoader().loadClass("MyClass");
    }
}

Почему это работает?Что ж, семантический анализ класса MyOuter не вызывает никаких проблем, потому что все типы известны на данном этапе.Вот почему загрузка класса MyOuter завершается успешно, и это приводит к тому, что экземпляр обработчика сценариев Groovy знает, что такое типы MyOuter и MyOuter.MyInner.Поэтому, когда вы в следующий раз загрузите MyClass из того же обработчика сценариев Groovy, он применит другую стратегию разрешения - он найдет оба класса доступными для текущего модуля компиляции, и ему не нужно будет разрешать класс MyOuter на основе его файла сценария Groovy.

Отладка

Если вы хотите лучше изучить этот вариант использования, стоит запустить отладчик и посмотреть, что происходит во время выполнения.Например, вы можете создать точку останова в строке 357 файла ResolveVisitor.java, чтобы увидеть описанный сценарий в действии.Имейте в виду, что одна вещь - resolveFromDefaultImports(type, testDefaultImports) будет пытаться искать классы MyClass и MyOuter, применяя пакеты по умолчанию, такие как java.util, java.io, groovy.lang и т. Д. Эта стратегия разрешения срабатывает раньше, чем resolveToOuter(type), поэтомуВы должны терпеливо прыгать через них.Но стоит посмотреть и лучше понять, как все работает.Надеюсь, это поможет!

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