`
JohnnyJian
  • 浏览: 106061 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Groovy深入探索——Groovy的ClassLoader体系

阅读更多
Groovy中定义了不少ClassLoader,本文将介绍其中绝大多数Groovy脚本都会涉及到的,也是最主要的3个ClassLoader:RootLoader、GroovyClassLoader和GroovyClassLoader.InnerLoader。

注:以下分析的Groovy源代码来自Groovy 2.1.3。

Java的ClassLoader

顾名思义,Java的ClassLoader就是类的装载器,它使JVM可以动态的载入Java类,JVM并不需要知道从什么地方(本地文件、网络等)载入Java类,这些都由ClassLoader完成。

可以说,ClassLoader是Class的命名空间。同一个名字的类可以由多个ClassLoader载入,由不同ClassLoader载入的相同名字的类将被认为是不同的类;而同一个ClassLoader对同一个名字的类只能载入一次。

Java的ClassLoader有一个著名的双亲委派模型(Parent Delegation Model):除了Bootstrap ClassLoader外,每个ClassLoader都有一个parent的ClassLoader,沿着parent最终会追索到Bootstrap ClassLoader;当一个ClassLoader要载入一个类时,会首先委派给parent,如果parent能载入这个类,则返回,否则这个ClassLoader才会尝试去载入这个类。

Java的ClassLoader体系如下,其中箭头指向的是该ClassLoader的parent:
Bootstrap ClassLoader
         ↑
Extension ClassLoader
         ↑
System ClassLoader
         ↑
User Custom ClassLoader  // 不一定有

更多关于Java的ClassLoader的信息请参考以下资料:


Groovy的ClassLoader

我们首先通过一个脚本来看一下,一个Groovy脚本的ClassLoader以及它的祖先们分别是什么:
def cl = this.class.classLoader
while (cl) {
    println cl
    cl = cl.parent
}

输出如下:
groovy.lang.GroovyClassLoader$InnerLoader@18622f3
groovy.lang.GroovyClassLoader@147c1db
org.codehaus.groovy.tools.RootLoader@186db54
sun.misc.Launcher$AppClassLoader@192d342
sun.misc.Launcher$ExtClassLoader@6b97fd

我们从而得出Groovy的ClassLoader体系:
            null                      // 即Bootstrap ClassLoader
             ↑
sun.misc.Launcher.ExtClassLoader      // 即Extension ClassLoader
             ↑
sun.misc.Launcher.AppClassLoader      // 即System ClassLoader
             ↑
org.codehaus.groovy.tools.RootLoader  // 以下为User Custom ClassLoader
             ↑
groovy.lang.GroovyClassLoader
             ↑
groovy.lang.GroovyClassLoader.InnerLoader

下面我们分别介绍一下RootLoader、GroovyClassLoader和GroovyClassLoader.InnerLoader。

Groovy脚本启动过程

要介绍RootLoader前,我们需要介绍一下Groovy脚本的启动过程。

当我们在命令行输入“groovy SomeScript”来运行脚本时,调用的是shell脚本$GROOVY_HOME/bin/groovy:
# ...
startGroovy groovy.ui.GroovyMain "$@"

其中startGroovy定义在$GROOVY_HOME/bin/startGroovy中:
# ...
STARTER_CLASSPATH="$GROOVY_HOME/lib/groovy-2.1.3.jar"
# ...
startGroovy ( ) {
    CLASS=$1
    shift
    # Start the Profiler or the JVM
    if $useprofiler ; then
        runProfiler
    else
        exec "$JAVACMD" $JAVA_OPTS \
            -classpath "$STARTER_CLASSPATH" \
            -Dscript.name="$SCRIPT_PATH" \
            -Dprogram.name="$PROGNAME" \
            -Dgroovy.starter.conf="$GROOVY_CONF" \
            -Dgroovy.home="$GROOVY_HOME" \
            -Dtools.jar="$TOOLS_JAR" \
            $STARTER_MAIN_CLASS \
            --main $CLASS \
            --conf "$GROOVY_CONF" \
            --classpath "$CP" \
            "$@"
    fi
}

STARTER_MAIN_CLASS=org.codehaus.groovy.tools.GroovyStarter

我们可以发现,这里其实是通过java启动了org.codehaus.groovy.tools.GroovyStarter,然后把“--main groovy.ui.GroovyMain”作为参数传给GroovyStarter,最后又把SomeScript作为参数传给GroovyMain。注意,这里只把$GROOVY_HOME/lib/groovy-2.1.3.jar作为classpath参数传给了JVM,而不包含Groovy依赖的第三方jar包。

我们来看一下GroovyStarter的源代码(其中省略了异常处理的代码):
public static void rootLoader(String args[]) {
    String conf = System.getProperty("groovy.starter.conf",null);
    LoaderConfiguration lc = new LoaderConfiguration();
    // 这里省略了解析命令行参数的代码...
    // load configuration file
    if (conf!=null) {
        lc.configure(new FileInputStream(conf));
    }
    // create loader and execute main class
    ClassLoader loader = new RootLoader(lc);
    Class c = loader.loadClass(lc.getMainClass()); // 使用RootLoader载入GroovyMain
    Method m = c.getMethod("main", new Class[]{String[].class});
    m.invoke(null, new Object[]{newArgs}); // 调用GroovyMain的main方法
}
// ...
public static void main(String args[]) {
    rootLoader(args);
}

这里的LoaderConfiguration是用来做什么的呢?它是用来解析$GROOVY_HOME/conf/groovy-starter.conf文件的,该文件内容如下(去掉了注释部分):
load !{groovy.home}/lib/*.jar
load !{user.home}/.groovy/lib/*.jar
load ${tools.jar}

这表示,将$GROOVY_HOME/lib/*.jar、$HOME/.groovy/lib/*.jar以及tools.jar加入到RootLoader的classpath中,可以看出,这里包含了Groovy依赖的第三方jar包。

接下来,我们来看一下GroovyMain的源代码。GroovyMain的main函数进去之后,最终会到达processOnce方法:
private void processOnce() throws CompilationFailedException, IOException {
    GroovyShell groovy = new GroovyShell(conf);

    if (isScriptFile) {
        if (isScriptUrl(script)) {
            groovy.run(getText(script), script.substring(script.lastIndexOf("/") + 1), args);
        } else {
            groovy.run(huntForTheScriptFile(script), args); // 本地脚本文件执行这行
        }
    } else {
        groovy.run(script, "script_from_command_line", args);
    }
}

可以看到,GroovyMain是通过GroovyShell来执行脚本文件的,GroovyShell的具体执行脚本的代码我们不再分析,我们只看GroovyShell的构造函数中初始化ClassLoader的代码:
final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader();
this.loader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() {
    public GroovyClassLoader run() {
        return new GroovyClassLoader(parentLoader,config);
    }
});

由此可见,GroovyShell使用了GroovyClassLoader来加载类,而该GroovyClassLoader的parent即为GroovyShell的ClassLoader,也就是GroovyMain的ClassLoader,也就是RootLoader。

最后来总结一下Groovy脚本的启动流程(括号中表示使用的ClassLoader):
GroovyStarter
    ↓ (RootLoader)
GroovyMain
    ↓
GroovyShell
    ↓ (GroovyClassLoader)
SomeScript

RootLoader

RootLoader作为Groovy的根ClassLoader,负责加载Groovy及其依赖的第三方库中的类。它管理了Groovy的classpath,我们可以通过$GROOVY_HOME/conf/groovy-starter.conf文件或groovy的命令行参数“-classpath”往其中添加路径。注意,这有别于java的命令行参数“-classpath”定义的classpath,RootLoader中的classpath对Java原有的ClassLoader是不可见的。

我们先通过一个脚本来看一下RootLoader是如何体现为Groovy的classpath管理者的:
class C {}

println this.class.classLoader
println C.classLoader
println()

println groovy.ui.GroovyMain.classLoader
println org.objectweb.asm.ClassVisitor.classLoader
println()

println String.classLoader
println()

println org.codehaus.groovy.tools.GroovyStarter.classLoader
println ClassLoader.systemClassLoader.findLoadedClass('org.codehaus.groovy.tools.GroovyStarter')?.classLoader
println()

输出如下:
groovy.lang.GroovyClassLoader$InnerLoader@1ba6076
groovy.lang.GroovyClassLoader$InnerLoader@1ba6076

org.codehaus.groovy.tools.RootLoader@a97b0b
org.codehaus.groovy.tools.RootLoader@a97b0b

null

org.codehaus.groovy.tools.RootLoader@a97b0b
sun.misc.Launcher$AppClassLoader@192d342

  • 脚本类和C类的ClassLoader是GroovyClassLoader.InnerLoader,这是我们预期内的。
  • GroovyMain的ClassLoader是RootLoader,是因为GroovyStarter就是用RootLoader来加载它的;而ClassVisitor是Groovy依赖的asm库中的类,这个库的jar文件路径不在Java的classpath中,而是在Groovy的classpath中,所以很自然的,它的ClassLoader也是RootLoader。
  • String的ClassLoader是null,这是因为JDK中的基本类型都必须由Bootstrap ClassLoader加载(如果允许自定义的ClassLoader加载,那就天下大乱了)。
  • GroovyStarter的ClassLoader是RootLoader,这点让我们很意外,GroovyStarter应该已经由System ClassLoader载入(systemClassLoader.findLoadedClass证实了这个想法),根据双亲委派模型,System ClassLoader的后代都不会尝试去加载这个类,为什么RootLoader又去加载了一次GroovyStarter呢?

答案很简单,因为RootLoader没有遵循双亲委派模型。我们来看一下RootLoader的loadClass方法(做了一些简单的方法展开):
protected synchronized Class loadClass(final String name, boolean resolve) throws ClassNotFoundException {
    Class c = this.findLoadedClass(name);
    if (c != null) return c;
    c = (Class) customClasses.get(name); // customClasses定义了一些必须由Java原有ClassLoader载入的类
    if (c != null) return c;

    try {
        c = super.findClass(name); // 先尝试加载这个类
    } catch (ClassNotFoundException cnfe) {
        // IGNORE
    }
    if (c == null) c = super.loadClass(name, resolve); // 加载不到则回到原有的双亲委派模型

    if (resolve) resolveClass(c);

    return c;
}

RootLoader先尝试加载类,如果加载不到,再委派给parent加载,所以即使parent已经载入了GroovyStarter,RootLoader还会再加载一次。

为什么要这样做的?道理很简单。在前文中,我一再提醒大家,Java的classpath中只包含了Groovy的jar包,而不包含Groovy依赖的第三方jar包,而Groovy的classpath则包含了Groovy以及其依赖的所有第三方jar包。如果RootLoader使用双亲委派模型,那么Groovy的jar包中的类就会由System ClassLoader加载,当解析Groovy的类时,需要加载第三方的jar包,这时System ClassLoader并不知道从哪里加载,导致找不到类。因此RootLoader并没有使用双亲委派模型。

可能你有疑问:为什么不把这些jar包都加入Java的classpath中?这样不就不会有这个问题了吗?确实如此,但是Groovy可以通过多种方式更灵活的往自己的classpath中添加路径(你甚至可以通过代码往RootLoader的classpath中添加路径),而Java的classpath只能通过命令行添加,因此就有了RootLoader这样的设计。

GroovyClassLoader

GroovyClassLoader主要负责在运行时编译groovy源代码为Class的工作,从而使Groovy实现了将groovy源代码动态加载为Class的功能。

GroovyClassLoader编译groovy代码的工作重要集中到doParseClass方法中:
private Class doParseClass(GroovyCodeSource codeSource) {
    validate(codeSource); // 简单校验一些参数是否为null
    Class answer;  // Was neither already loaded nor compiling, so compile and add to cache.
    CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource());
    SourceUnit su = null;
    if (codeSource.getFile() == null) {
        su = unit.addSource(codeSource.getName(), codeSource.getScriptText());
    } else {
        su = unit.addSource(codeSource.getFile());
    }

    ClassCollector collector = createCollector(unit, su); // 这里创建了InnerLoader
    unit.setClassgenCallback(collector);
    int goalPhase = Phases.CLASS_GENERATION;
    if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT;
    unit.compile(goalPhase); // 编译groovy源代码

    // 查找源文件中的Main Class
    answer = collector.generatedClass;
    String mainClass = su.getAST().getMainClassName();
    for (Object o : collector.getLoadedClasses()) {
        Class clazz = (Class) o;
        String clazzName = clazz.getName();
        definePackage(clazzName);
        setClassCacheEntry(clazz);
        if (clazzName.equals(mainClass)) answer = clazz;
    }
    return answer;
}

如何编译groovy源代码已超出本文的范畴,因此不再介绍具体过程。

GroovyClassLoader.InnerLoader

我们继续来看一下GroovyClassLoader的createCollector方法:
protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
    InnerLoader loader = AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() {
        public InnerLoader run() {
            return new InnerLoader(GroovyClassLoader.this);
        }
    });
    return new ClassCollector(loader, unit, su);
}

public static class ClassCollector extends CompilationUnit.ClassgenCallback {
    private final GroovyClassLoader cl;
    // ...
    protected ClassCollector(InnerLoader cl, CompilationUnit unit, SourceUnit su) {
        this.cl = cl;
        // ...
    }
    public GroovyClassLoader getDefiningClassLoader() {
        return cl;
    }
    protected Class createClass(byte[] code, ClassNode classNode) {
        GroovyClassLoader cl = getDefiningClassLoader();
        Class theClass = cl.defineClass(classNode.getName(), code, 0, code.length, unit.getAST().getCodeSource()); // 通过InnerLoader加载该类
        this.loadedClasses.add(theClass);
        // ...
        return theClass;
    }
    // ...
}

我们可以看出,ClassCollector的作用,就是在编译的过程中,将编译出来的字节码,通过InnerLoader进行加载。另外,每次编译groovy源代码的时候,都会新建一个InnerLoader的实例。

InnerLoader是如何加载这些类的呢?它将所有的加载工作又委派回给GroovyClassLoader。由于InnerLoader的代码简单,这里就不贴出来了。

那有了GroovyClassLoader,为什么还需要InnerLoader呢?主要有两个原因:

  • 由于一个ClassLoader对于同一个名字的类只能加载一次,如果都由GroovyClassLoader加载,那么当一个脚本里定义了C这个类之后,另外一个脚本再定义一个C类的话,GroovyClassLoader就无法加载了。
  • 由于当一个类的ClassLoader被GC之后,这个类才能被GC,如果由GroovyClassLoader加载所有的类,那么只有当GroovyClassLoader被GC了,所有这些类才能被GC,而如果用InnerLoader的话,由于编译完源代码之后,已经没有对它的外部引用,除了它加载的类,所以只要它加载的类没有被引用之后,它以及它加载的类就都可以被GC了。

总结

本文介绍了Groovy中最主要的3个ClassLoader:

  • RootLoader:管理了Groovy的classpath,负责加载Groovy及其依赖的第三方库中的类,它不是使用双亲委派模型。
  • GroovyClassLoader:负责在运行时编译groovy源代码为Class的工作,从而使Groovy实现了将groovy源代码动态加载为Class的功能。
  • GroovyClassLoader.InnerLoader:Groovy脚本类的直接ClassLoader,它将加载工作委派给GroovyClassLoader,它的存在是为了支持不同源码里使用相同的类名,以及加载的类能顺利被GC。

以上分析有不当之处敬请指出,谢谢大家的阅读。
分享到:
评论

相关推荐

    Java调用Groovy,实时动态加载数据库groovy脚本

    它继承自Java的ClassLoader,能解析Groovy源码并生成字节码。 3. 加载并执行Groovy脚本:通过GroovyClassLoader的`parseClass()`方法解析Groovy源码,然后使用`newInstance()`方法创建脚本实例,最后通过`...

    java在嵌入运行groovy代码1

    它是一个 ClassLoader 的子类,可以解析 Groovy 源代码并生成对应的 Class 对象。如果你的 Groovy 脚本包含类的定义,使用 GroovyClassLoader 更为合适。 3. JSR-223(Java Scripting API)是 Java 平台的标准,...

    gretty-groovy-classloader:最小的示例来重现gretty groovy classloader的问题

    重现Tomcat 8上的groovy类加载器问题的最小示例。 此示例使用Spring Boot重现了该问题。 有关没有Spring Boot的示例,请参阅: : 作品:./gradlew jettyRun 常规类失败:./gradlew tomcatRun Gretty也会崩溃,...

    GroovyAction:通过 groovy 编写动作

    其实具体的工作量并不大,主要就是支持脚本语言groovy编写的action,扩充了ObjectFactory,增加了groovy的classloader,同时扩展了配置项,分为生产环境和运行环境,在生产环境中,不把class进行缓存,可以动态展现...

    DelegatingGroovyClassloader

    目标是能够暴露一个普通的 ClassLoader 并且能够卸载不同的如果需要,加载类以及所有 Groovy 环境。 为了进行完整的演示,我使用了一个通用的合约,接口sample.module.RenderingModule, 和一个简单的 Main 类,...

    安卓Android源码——引用第三方库的方法.zip

    本教程将深入探讨在Android源码中引用第三方库的方法,帮助开发者更好地理解和实践这一关键技能。 首先,理解第三方库的概念是至关重要的。第三方库是由非官方、独立开发者创建的代码库,可以为Android应用程序提供...

    通过自定义Gradle插件修改编译后的class文件

    在Java开发中,Gradle是一种广泛应用的构建自动化工具,它允许开发者通过编写Groovy或Kotlin DSL脚本来管理项目的构建过程。自定义Gradle插件是Gradle的强大特性之一,可以扩展其功能以满足特定项目需求。本篇将详细...

    深入JVM内核—原理、诊断与优化视频教程-2.JVM运行机制

    类加载器主要有Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader,它们遵循双亲委派模型,保证类的唯一性。 2. **内存区域划分**:JVM内存分为堆内存和栈内存,其中栈内存又包括方法区、虚拟机栈、...

    Java虚拟机规范(Java SE 7)中文版

    基础类加载器包括Bootstrap ClassLoader、Extension ClassLoader和Application ClassLoader,它们协同工作,确保类的正确加载。 2. **字节码验证**:JVM在执行字节码之前会进行验证,确保代码的安全性,防止恶意...

    arthas,懂得都懂,直接解压用就完事儿了

    2. **在线脚本编辑**:Arthas支持在线编辑和执行Groovy或JavaScript脚本,这对于动态解决问题或者临时调整应用行为非常有用。 3. **Classloader诊断**:Java的类加载机制有时会带来困扰,Arthas能帮助开发者理解类...

    一篇文章带你搞定SpringBoot中的热部署devtools方法

    ```groovy compileOnly 'org.springframework.boot:spring-boot-devtools' ``` 注意,`optional`属性设置为`true`是为了避免在生产环境中包含这个依赖。 完成依赖添加后,我们可以通过一个简单的例子来测试热部署...

    arthas-离线版.tar.gz

    1. **命令行工具**:Arthas提供了丰富的命令行工具,如`asm`、`classloader`、`console`、`dump`等,方便开发者在不重启应用的情况下查看运行时信息,如类加载器、方法、变量等。 2. **JVM监控**:通过`jvm`命令,...

    Java虚拟家规范《Java SE 7 中文版》.zip

    《Java SE 7 中文版》这份文档详细阐述了Java虚拟机的规范,旨在帮助开发者深入理解JVM的工作原理,从而优化代码性能,解决运行时问题。 在Java SE 7版本中,JVM规范涵盖了许多关键领域: 1. **类文件结构**:Java...

    java动态性.rar

    7. 动态语言支持(Dynamic Language Support):Java平台也支持其他动态语言,如JRuby、Groovy等,它们可以在JVM上运行,利用Java的动态性同时享受动态语言的便利。 8. invokedynamic指令:Java 7引入了一种新的...

    JDK11-java-virtual-machine-guide.pdf

    1. **类加载机制**:JVM通过类加载器(ClassLoader)将类文件加载到内存中,分为启动类加载器、扩展类加载器和应用程序类加载器。理解类加载过程、双亲委派模型以及自定义类加载器对于优化和调试应用程序至关重要。 ...

    xmljava系统源码-java-trader:Java期货交易者CTPFEMASXTP

    使用动态ClassLoader加载交易策略实现类, 允许运行时动态更新 支持基于GROOVY的脚本式策略编程, 可运行时动态更新, 支持自定义函数插件式扩展 交易策略通过简单的分组和配置参数调整, 动态组合动态调整, 最大限度...

    springloaded-1.2.8.RELEASE

    SpringLoaded的核心功能是类加载器(ClassLoader)的增强,它能够监测到源代码的变化,并在代码保存后立即重新编译和加载新的类到运行中的应用程序。这样就避免了传统方式下需要停止、重新构建和启动服务的繁琐过程,...

    arthas最新版本arthas-packaging-3.7.0-bin

    本文将深入探讨Arthas最新版本`arthas-packaging-3.7.0-bin`的相关知识点。 首先,我们来看一下压缩包中的文件: 1. **as-service.bat** 和 **as.sh**:这两个文件分别是Windows和Unix/Linux环境下的启动脚本,...

    arthas-packaging-3.1.1-bin.zip

    2. **类加载器视图**:`classloader`命令可帮助理解类加载机制,追踪类的加载来源,解决类冲突问题。 3. **在线脚本编辑**:`script`命令允许用户编写并执行Groovy等脚本,对运行时环境进行动态操作。 4. **方法...

    CodeEval:Java中一些代码eval问题的一些解决方案

    ClassLoader loader = new URLClassLoader(new URL[]{fileManager.getLocation(StandardLocation.CLASS_OUTPUT).toURI().toURL()}, Thread.currentThread().getContextClassLoader()); Class&lt;?&gt; dynamicClass = ...

Global site tag (gtag.js) - Google Analytics