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以及它的祖先们分别是什么: 1 def cl = this.class.classLoader 2 while (cl) { 3 println cl 4 cl = cl.parent 5 } 输出如下: 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: 1 # 2 startGroovy groovy.ui.GroovyMain "$@" 其中startGroovy定义在$GROOVY_HOME/bin/startGroovy中: 1 # 2 STARTER_CLASSPATH="$GROOVY_HOME/lib/groovy-2.1.3.jar" 3 # 4 startGroovy ( ) { 5 CLASS=$1 6 shift 7 # Start the Profiler or the JVM 8 if $useprofiler ; then 9 runProfiler 10 else 11 exec "$JAVACMD" $JAVA_OPTS \ 12 -classpath "$STARTER_CLASSPATH" \ 13 -Dscript.name="$SCRIPT_PATH" \ 14 -Dprogram.name="$PROGNAME" \ 15 -Dgroovy.starter.conf="$GROOVY_CONF" \ 16 -Dgroovy.home="$GROOVY_HOME" \ 17 -Dtools.jar="$TOOLS_JAR" \ 18 $STARTER_MAIN_CLASS \ 19 --main $CLASS \ 20 --conf "$GROOVY_CONF" \ 21 --classpath "$CP" \ 22 "$@" 23 fi 24 } 25 26 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的源代码(其中省略了异常处理的代码): 1 public static void rootLoader(String args[]) { 2 String conf = System.getProperty("groovy.starter.conf",null); 3 LoaderConfiguration lc = new LoaderConfiguration(); 4 // 这里省略了解析命令行参数的代码 5 // load configuration file 6 if (conf!=null) { 7 lc.configure(new FileInputStream(conf)); 8 } 9 // create loader and execute main class 10 ClassLoader loader = new RootLoader(lc); 11 Class c = loader.loadClass(lc.getMainClass()); // 使用RootLoader载入GroovyMain 12 Method m = c.getMethod("main", new Class[]{String[].class}); 13 m.invoke(null, new Object[]{newArgs}); // 调用GroovyMain的main方法 14 } 15 // 16 public static void main(String args[]) { 17 rootLoader(args); 18 } 这里的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方法: 1 private void processOnce() throws CompilationFailedException, IOException { 2 GroovyShell groovy = new GroovyShell(conf); 3 4 if (isScriptFile) { 5 if (isScriptUrl(script)) { 6 groovy.run(getText(script), script.substring(script.lastIndexOf("/") + 1), args); 7 } else { 8 groovy.run(huntForTheScriptFile(script), args); // 本地脚本文件执行这行 9 } 10 } else { 11 groovy.run(script, "script_from_command_line", args); 12 } 13 } 可以看到,GroovyMain是通过GroovyShell来执行脚本文件的,GroovyShell的具体执行脚本的代码我们不再分析,我们只看GroovyShell的构造函数中初始化ClassLoader的代码: 1 final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader(); 2 this.loader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() { 3 public GroovyClassLoader run() { 4 return new GroovyClassLoader(parentLoader,config); 5 } 6 }); 由此可见,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管理者的: 1 class C {} 2 3 println this.class.classLoader 4 println C.classLoader 5 println() 6 7 println groovy.ui.GroovyMain.classLoader 8 println org.objectweb.asm.ClassVisitor.classLoader 9 println() 10 11 println String.classLoader 12 println() 13 14 println org.codehaus.groovy.tools.GroovyStarter.classLoader 15 println ClassLoader.systemClassLoader.findLoadedClass('org.codehaus.groovy.tools.GroovyStarter')?.classLoader 16 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
答案很简单,因为RootLoader没有遵循双亲委派模型。我们来看一下RootLoader的loadClass方法(做了一些简单的方法展开): 1 protected synchronized Class loadClass(final String name, boolean resolve) throws ClassNotFoundException { 2 Class c = this.findLoadedClass(name); 3 if (c != null) return c; 4 c = (Class) customClasses.get(name); // customClasses定义了一些必须由Java原有ClassLoader载入的类 5 if (c != null) return c; 6 7 try { 8 c = super.findClass(name); // 先尝试加载这个类 9 } catch (ClassNotFoundException cnfe) { 10 // IGNORE 11 } 12 if (c == null) c = super.loadClass(name, resolve); // 加载不到则回到原有的双亲委派模型 13 14 if (resolve) resolveClass(c); 15 16 return c; 17 } 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方法中: 1 private Class doParseClass(GroovyCodeSource codeSource) { 2 validate(codeSource); // 简单校验一些参数是否为null 3 Class answer; // Was neither already loaded nor compiling, so compile and add to cache. 4 CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource()); 5 SourceUnit su = null; 6 if (codeSource.getFile() == null) { 7 su = unit.addSource(codeSource.getName(), codeSource.getScriptText()); 8 } else { 9 su = unit.addSource(codeSource.getFile()); 10 } 11 12 ClassCollector collector = createCollector(unit, su); // 这里创建了InnerLoader 13 unit.setClassgenCallback(collector); 14 int goalPhase = Phases.CLASS_GENERATION; 15 if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT; 16 unit.compile(goalPhase); // 编译groovy源代码 17 18 // 查找源文件中的Main Class 19 answer = collector.generatedClass; 20 String mainClass = su.getAST().getMainClassName(); 21 for (Object o : collector.getLoadedClasses()) { 22 Class clazz = (Class) o; 23 String clazzName = clazz.getName(); 24 definePackage(clazzName); 25 setClassCacheEntry(clazz); 26 if (clazzName.equals(mainClass)) answer = clazz; 27 } 28 return answer; 29 } 如何编译groovy源代码已超出本文的范畴,因此不再介绍具体过程。 GroovyClassLoader.InnerLoader 我们继续来看一下GroovyClassLoader的createCollector方法: 1 protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) { 2 InnerLoader loader = AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() { 3 public InnerLoader run() { 4 return new InnerLoader(GroovyClassLoader.this); 5 } 6 }); 7 return new ClassCollector(loader, unit, su); 8 } 9 10 public static class ClassCollector extends CompilationUnit.ClassgenCallback { 11 private final GroovyClassLoader cl; 12 // 13 protected ClassCollector(InnerLoader cl, CompilationUnit unit, SourceUnit su) { 14 this.cl = cl; 15 // 16 } 17 public GroovyClassLoader getDefiningClassLoader() { 18 return cl; 19 } 20 protected Class createClass(byte[] code, ClassNode classNode) { 21 GroovyClassLoader cl = getDefiningClassLoader(); 22 Class theClass = cl.defineClass(classNode.getName(), code, 0, code.length, unit.getAST().getCodeSource()); // 通过InnerLoader加载该类 23 this.loadedClasses.add(theClass); 24 // 25 return theClass; 26 } 27 // 28 } 我们可以看出,ClassCollector的作用,就是在编译的过程中,将编译出来的字节码,通过InnerLoader进行加载。另外,每次编译groovy源代码的时候,都会新建一个InnerLoader的实例。 InnerLoader是如何加载这些类的呢?它将所有的加载工作又委派回给GroovyClassLoader。由于InnerLoader的代码简单,这里就不贴出来了。 那有了GroovyClassLoader,为什么还需要InnerLoader呢?主要有两个原因:
总结 本文介绍了Groovy中最主要的3个ClassLoader:
以上分析有不当之处敬请指出,谢谢大家的阅读。 |
|