Вы столкнулись с крайним случаем здесь.Чтобы выяснить, что происходит, давайте определим начальные условия:
- у вас есть класс 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)
, поэтомуВы должны терпеливо прыгать через них.Но стоит посмотреть и лучше понять, как все работает.Надеюсь, это поможет!