在加载使用其他类的静态内部类的类时,GroovyScriptEngine抛出MultipleCompilationErrorsException

伦尼·巴雷特

我遇到了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的命令行中正常编译。

西蒙·斯蒂芬尼克

您在这里遇到了一个极端情况。为了澄清会发生什么,让我们定义初始条件:

  • 您有一个在JVM内部执行的Java(或Groovy)类
  • 您有两个在JVM外部加载的Groovy类

如果将这两个Groovy类放在执行Java类的相同路径中,则不会存在您描述的问题-在这种情况下,IDE会小心地编译这些Groovy类并将它们放入开始使用的JVM的类路径中运行您的Java测试类。

但这不是您的情况,您正在尝试使用GroovyClassLoader(扩展URLClassLoaderbtw)在运行的JVM之外加载这两个Groovy类我将尝试用最简单的词来解释发生什么情况,即类型的添加字段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

该方法同时针对MyOuterMyOuter.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与此脚本文件名关联对象。就像说“好吧,该类目前不在我的类路径中,但是有一个源文件,我可以看到,一旦编译,它将提供有效的名称类型MyOuter这就是为什么它最终达到的原因:

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

currentClassMyClass类型相关联的对象在哪里-它将此源单元添加到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脚本引擎实例知道类型MyOuterMyOuter.MyInner类型的原因。因此,当您下次MyClass从同一个Groovy脚本引擎加载时,它将应用不同的解析策略-它会找到当前编译单元可用的两个类,并且不必MyOuter根据其Groovy脚本文件来解析类。

调试

如果您想更好地研究此用例,则值得运行调试器并查看在运行时发生的情况。例如,您可以在ResolveVisitor.java文件的第357行创建一个断点,以查看所描述的实际情况。请记住一两件事,虽然-resolveFromDefaultImports(type, testDefaultImports)将尝试查找MyClassMyOuter类应用默认包一样java.utiljava.iogroovy.lang之前等,这决心策略踢resolveToOuter(type),所以你必须耐心地通过他们跳。但是值得一看,并更好地了解事物的运作方式。希望能帮助到你!

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章