源码分析基于hadoop-2.4.0版本,只是一个学习过程,希望自己能清楚理解这部分内容;
bin/hadoop jar xxx.jar mainclass args
……
每次写好一个Project或对Project做修改后,都必须打个Jar包,然后再用上面的命令提交到HadoopCluster上去运行,在开发阶段那是极其繁琐的。程序员是“最懒”的,既然麻烦肯定是要想些法子减少无谓的键盘敲击,顺带延长键盘寿命。比如有的人就写了些Shell脚本来自动编译、打包,然后提交到Hadoop。但还是稍显麻烦,目前比较方便的方法就是用Hadoopeclipse plugin,可以浏览管理HDFS,自动创建MR程序的模板文件,最爽的就是直接Run on hadoop了。还有一款叫Hadoop Studio的软件,看上去貌似是蛮强大,但是没试过,这里不做评论。那么它们是怎么做到不用上面那个命令来提交作业的呢?不知道?没关系,开源的嘛,不懂得就直接看源码分析,这就是开源软件的最大利处。
我们首先从bin/hadoop这个Shell脚本开始分析,看这个脚本内部到底做了什么,如何来提交Hadoop作业的。
因为是Java程序,这个脚本最终都是要调用Java来运行的,所以这个脚本最重要的就是添加一些前置参数,如CLASSPATH等。所以,我们直接跳到这个脚本的最后一行,看它到底添加了那些参数,然后再逐个分析。
# run it
exec "$JAVA" $JAVA_HEAP_MAX $HADOOP_OPTS $CLASS "$@"
从上面这行命令我们可以看到这个脚本最终添加了如下几个重要参数:JAVA_HEAP_MAX、HADOOP_OPTS、CLASSPATH、CLASS。下面我们来一个个的分析。
首先是JAVA_HEAP_MAX,这个就比较简单了,主要涉及代码如下:
JAVA_HEAP_MAX=-Xmx1000m
# check envvars which might override default args
if [ "$HADOOP_HEAPSIZE" !="" ]; then
#echo "run with heapsize $HADOOP_HEAPSIZE"
JAVA_HEAP_MAX="-Xmx""$HADOOP_HEAPSIZE""m"
#echo $JAVA_HEAP_MAX
fi
首先赋予默认值-Xmx1000m,然后检查hadoop-env.sh中是否设置并导出了HADOOP_HEAPSIZE,如果有的话,就使用该值覆盖,得到最后的JAVA_HEAP_MAX。
接着是分析CLASSPATH,这是这个脚本的重点之一。这部分主要就是添加了相应依赖库和配置文件到CLASSPATH。
# 首先用Hadoop的配置文件目录初始化CLASSPATH
CLASSPATH="${HADOOP_CONF_DIR}"
……
# 下面是针对于Hadoop发行版,添加Hadoop核心Jar包和webapps到CLASSPATH
if [ -d"$HADOOP_HOME/webapps" ]; then
CLASSPATH=${CLASSPATH}:$HADOOP_HOME
fi
for f in $HADOOP_HOME/hadoop-*-core.jar; do
CLASSPATH=${CLASSPATH}:$f;
done
# 添加libs里的Jar包
for fin $HADOOP_HOME/lib/*.jar; do
CLASSPATH=${CLASSPATH}:$f;
Done
for f in $HADOOP_HOME/lib/jsp-2.1/*.jar; do
CLASSPATH=${CLASSPATH}:$f;
done
# 下面的TOOL_PATH只在命令为“archive”时才添加到CLASSPATH
for fin $HADOOP_HOME/hadoop-*-tools.jar; do
TOOL_PATH=${TOOL_PATH}:$f;
done
for f in$HADOOP_HOME/build/hadoop-*-tools.jar; do
TOOL_PATH=${TOOL_PATH}:$f;
done
# 最后添加用户的自定义Hadoop Classpath
if ["$HADOOP_CLASSPATH" != "" ]; then
CLASSPATH=${CLASSPATH}:${HADOOP_CLASSPATH}
fi
上面只分析一部分,由于代码比较长,针对开发者部分的CLASSPATH添加没有列出来。
下面是这个脚本的重点、实体之处:CLASS分析。Shell脚本会根据你输入的命令参数来设置CLASS和HADOOP_OPTS,其中CLASS所指向的类才是最终真正执行你的命令的实体。
# figure out which class to run
if [ "$COMMAND" ="namenode" ] ; then
CLASS='org.apache.hadoop.hdfs.server.namenode.NameNode'
HADOOP_OPTS="$HADOOP_OPTS$HADOOP_NAMENODE_OPTS"
……
elif [ "$COMMAND" = "fs" ] ; then
CLASS=org.apache.hadoop.fs.FsShell
HADOOP_OPTS="$HADOOP_OPTS$HADOOP_CLIENT_OPTS"
……
elif [ "$COMMAND" = "jar" ] ; then
CLASS=org.apache.hadoop.util.RunJar
……
elif [ "$COMMAND" = "archive" ] ; then
CLASS=org.apache.hadoop.tools.HadoopArchives
CLASSPATH=${CLASSPATH}:${TOOL_PATH}
HADOOP_OPTS="$HADOOP_OPTS$HADOOP_CLIENT_OPTS"
……
else
CLASS=$COMMAND
fi
这里我们要关心的就是"$COMMAND"= "jar"时对应的类org.apache.hadoop.util.RunJar,这个类等下我们继续分析,这是我们通向最终目标的下一个路口。
脚本在最后还设置了hadoop.log.dir、hadoop.log.file等HADOOP_OPTS。接着,就利用exec命令带上刚才的参数提交任务了。
通过对上面的分析,我们知道了,如果想取代这个脚本,那就必须至少把Hadoop依赖的库和配置文件目录给加到CLASSPATH中(JAVA_HEAP_MAX和HADOOP_OPTS不是必须的),然后调用org.apache.hadoop.util.RunJar类来提交Jar到Hadoop。
上面分析了bin/hadoop脚本,知道了提交一个Hadoop作业所需要的基本设置以及真正执行任务提交的类。现在我们就来分析这个提交任务的类org.apache.hadoop.util.RunJar,看它内部具体又做了些什么。
RunJar是Hadoop中的一个工具类,结构很简单,只有两个方法:main和unJar。我们从main开始一步步分析。
public static voidmain(String[] args)throws Throwable {
String usage = "RunJarjarFile [mainClass] args...";
if(args.length < 1) {
System.err.println(usage);
System.exit(-1);
}
intfirstArg = 0;
String fileName = args[firstArg++];
File file = newFile(fileName);
if(!file.exists() || !file.isFile()) {
System.err.println("Nota valid JAR: " + file.getCanonicalPath());
System.exit(-1);
}
String mainClassName = null;
JarFile jarFile;
try {
jarFile = newJarFile(fileName);
} catch(IOExceptionio) {
throw newIOException("Error opening job jar: " + fileName)
.initCause(io);
}
Manifest manifest = jarFile.getManifest();
if(manifest != null) {
mainClassName =manifest.getMainAttributes().getValue("Main-Class");
}
jarFile.close();
if(mainClassName == null) {
if(args.length < 2) {
System.err.println(usage);
System.exit(-1);
}
mainClassName = args[firstArg++];
}
mainClassName = mainClassName.replaceAll("/",".");
File tmpDir = newFile(new Configuration().get("hadoop.tmp.dir"));
ensureDirectory(tmpDir);
final FileworkDir;
try {
workDir = File.createTempFile("hadoop-unjar","",tmpDir);
} catch(IOException ioe) {
// If user has insufficient permsto write to tmpDir, default
// "Permissiondenied" message doesn't specify a filename.
System.err.println("Errorcreating temp dir in hadoop.tmp.dir "
+ tmpDir + "due to " + ioe.getMessage());
System.exit(-1);
return;
}
if(!workDir.delete()) {
System.err.println("Deletefailed for " + workDir);
System.exit(-1);
}
ensureDirectory(workDir);
ShutdownHookManager.get().addShutdownHook(
newRunnable() {
@Override
public void run(){
FileUtil.fullyDelete(workDir);
}
}, SHUTDOWN_HOOK_PRIORITY);
unJar(file, workDir);
ArrayList<URL> classPath = newArrayList<URL>();
classPath.add(newFile(workDir+"/").toURI().toURL());
classPath.add(file.toURI().toURL());
classPath.add(newFile(workDir,"classes/").toURI().toURL());
File[] libs = newFile(workDir,"lib").listFiles();
if(libs != null) {
for (int i =0; i < libs.length; i++) {
classPath.add(libs[i].toURI().toURL());
}
}
ClassLoader loader =
newURLClassLoader(classPath.toArray(new URL[0]));
Thread.currentThread().setContextClassLoader(loader);
Class<?> mainClass = Class.forName(mainClassName,true, loader);
Method main = mainClass.getMethod("main",newClass[] {
Array.newInstance(String.class,0).getClass()
});
String[] newArgs = Arrays.asList(args)
.subList(firstArg, args.length).toArray(newString[0]);
try {
main.invoke(null, newObject[] { newArgs });
} catch(InvocationTargetException e) {
throw e.getTargetException();
}
}
}
main首先检查传递参数是否符合要求,然后从第一个传递参数中获取jar包的名字,并试图从jar中包中获取manifest信息,以查找mainclassname。如果查找不到mainclassname,则把传递参数中的第二个设为mainclassname。
接下去,就是在"hadoop.tmp.dir"下创建一个临时文件夹,并挂载上关闭删除线程。这个临时文件夹用来放置解压后的jar包内容。jar包的解压工作由unJar方法完成,通过JarEntry逐个获取jar包内的内容,包括文件夹和文件,然后释放到临时文件夹中。
解压完毕后,开始做classpath的添加,依次把解压临时文件夹、传递进来的jar包、临时文件夹内的classes文件夹和lib里的所有jar包加入到classpath中。接着以这个classpath为搜索URL新建了一个URLClassLoader(要注意这个类加载器的parent包括了刚才bin/hadoop脚本提交时给的classpath),并设置为当前线程的上下文类加载器。
最后,利用Class.forName方法,以刚才的那个URLClassLoader为类加载器,动态生成一个mainclass的Class对象,并获取它的main方法,然后以传递参数中剩下的参数作为调用参数来调用这个main方法。
好了,从上分析看来,这个RunJar类是一个很简单的类,就是解压传递进来的jar包,再添加一些classpath,然后动态调用jar包里的mainclass的main方法。看到这里,我想你应该知道如何利用java代码来编写一个替代bin/hadoop的程序了,主要就是两步:
· 添加Hadoop的依赖库和配置文件;
· 解压jar包,再添加一些classpath,并动态调用相应方法。最偷懒的方法,直接用RunJar类就行了。