Groovy深入探索:Groovy的ClassLoader体系(1)

  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:

  BootstrapClassLoader   ↑   ExtensionClassLoader   ↑   SystemClassLoader   ↑   UserCustomClassLoader//不一定有

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

  Java Classloader   Understanding Extension Class Loading   Understanding Extension Class Loading   ClassLoader与JVM

  Groovy的ClassLoader

  我们首先通过一个脚本来看一下,一个Groovy脚本的ClassLoader以及它的祖先们分别是什么:

  defcl=this.class.classLoader   while(cl){   printlncl   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//即BootstrapClassLoader   ↑   sun.misc.Launcher.ExtClassLoader//即ExtensionClassLoader   ↑   sun.misc.Launcher.AppClassLoader//即SystemClassLoader   ↑   org.codehaus.groovy.tools.RootLoader//以下为UserCustomClassLoader   ↑   groovy.lang.GroovyClassLoader   ↑   groovy.lang.GroovyClassLoader.InnerLoader

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

  Groovy脚本启动过程

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

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

  #…   startGroovygroovy.ui.GroovyMain"$@"

  其中startGroovy定义在$GROOVY_HOME/bin/startGroovy中:

  #…   STARTER_CLASSPATH="$GROOVY_HOME/lib/groovy-2.1.3.jar"  #…   startGroovy(){   CLASS=$1  shift   #StarttheProfilerortheJVM   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的源代码(其中省略了异常处理的代码):

  publicstaticvoidrootLoader(Stringargs[]){   Stringconf=System.getProperty("groovy.starter.conf",null);   LoaderConfigurationlc=newLoaderConfiguration();   //这里省略了解析命令行参数的代码   //loadconfigurationfile   if(conf!=null){   lc.configure(newFileInputStream(conf));   }   //createloaderandexecutemainclass   ClassLoaderloader=newRootLoader(lc);   Classc=loader.loadClass(lc.getMainClass());//使用RootLoader载入GroovyMain   Methodm=c.getMethod("main",newClass[]{String[].class});   m.invoke(null,newObject[]{newArgs});//调用GroovyMain的main方法   }   //   publicstaticvoidmain(Stringargs[]){   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方法:

  privatevoidprocessOnce()throwsCompilationFailedException,IOException{   GroovyShellgroovy=newGroovyShell(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的代码:

  finalClassLoaderparentLoader=(parent!=null)?parent:GroovyShell.class.getClassLoader();   this.loader=AccessController.doPrivileged(newPrivilegedAction<GroovyClassLoader>(){   publicGroovyClassLoaderrun(){   returnnewGroovyClassLoader(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管理者的:

  classC{}   printlnthis.class.classLoader   printlnC.classLoader   println()   printlngroovy.ui.GroovyMain.classLoader   printlnorg.objectweb.asm.ClassVisitor.classLoader   println()   printlnString.classLoader   println()   printlnorg.codehaus.groovy.tools.GroovyStarter.classLoader   printlnClassLoader.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方法(做了一些简单的方法展开):

  protectedsynchronizedClassloadClass(finalStringname,booleanresolve)throwsClassNotFoundException{   Classc=this.findLoadedClass(name);   if(c!=null)returnc;   c=(Class)customClasses.get(name);//customClasses定义了一些必须由Java原有ClassLoader载入的类   if(c!=null)returnc;   try{   c=super.findClass(name);//先尝试加载这个类   }catch(ClassNotFoundExceptioncnfe){   //IGNORE   }   if(c==null)c=super.loadClass(name,resolve);//加载不到则回到原有的双亲委派模型   if(resolve)resolveClass(c);   returnc;   }

  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方法中:

  privateClassdoParseClass(GroovyCodeSourcecodeSource){   validate(codeSource);//简单校验一些参数是否为null   Classanswer;//Wasneitheralreadyloadednorcompiling,socompileandaddtocache.   CompilationUnitunit=createCompilationUnit(config,codeSource.getCodeSource());   SourceUnitsu=null;   if(codeSource.getFile()==null){   su=unit.addSource(codeSource.getName(),codeSource.getScriptText());   }else{   su=unit.addSource(codeSource.getFile());   }   ClassCollectorcollector=createCollector(unit,su);//这里创建了InnerLoader   unit.setClassgenCallback(collector);   intgoalPhase=Phases.CLASS_GENERATION;   if(config!=null&&config.getTargetDirectory()!=null)goalPhase=Phases.OUTPUT;   unit.compile(goalPhase);//编译groovy源代码   //查找源文件中的MainClass   answer=collector.generatedClass;   StringmainClass=su.getAST().getMainClassName();   for(Objecto:collector.getLoadedClasses()){   Classclazz=(Class)o;   StringclazzName=clazz.getName();   definePackage(clazzName);   setClassCacheEntry(clazz);   if(clazzName.equals(mainClass))answer=clazz;   }   returnanswer;   }

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

  GroovyClassLoader.InnerLoader

  我们继续来看一下GroovyClassLoader的createCollector方法:

  protectedClassCollectorcreateCollector(CompilationUnitunit,SourceUnitsu){   InnerLoaderloader=AccessController.doPrivileged(newPrivilegedAction<InnerLoader>(){   publicInnerLoaderrun(){   returnnewInnerLoader(GroovyClassLoader.this);   }   });   returnnewClassCollector(loader,unit,su);   }   publicstaticclassClassCollectorextendsCompilationUnit.ClassgenCallback{   privatefinalGroovyClassLoadercl;   //……   protectedClassCollector(InnerLoadercl,CompilationUnitunit,SourceUnitsu){   this.cl=cl;   //……   }   publicGroovyClassLoadergetDefiningClassLoader(){   returncl;   }   protectedClasscreateClass(byte[]code,ClassNodeclassNode){   GroovyClassLoadercl=getDefiningClassLoader();   ClasstheClass=cl.defineClass(classNode.getName(),code,0,code.length,unit.getAST().getCodeSource());//通过InnerLoader加载该类   this.loadedClasses.add(theClass);   //……   returntheClass;   }   //……   }

  我们可以看出,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。 到一个新的环境去欣赏去看去听,

Groovy深入探索:Groovy的ClassLoader体系(1)

相关文章:

你感兴趣的文章:

标签云: