学习Hadoop Common模块,当然应该是从最简单,最基础的模块学习最好,所以我挑选了其中的conf配置模块进行学习。整体的类结构非常简单。
只要继承了Configurable接口,一般表明就是可配置的,可以执行相应的配置操作,但是配置的集中操作的体现是在Configuration这个类中。这个类中定义了很多的集合变量:
/**
* List of configuration resources.
*/
private ArrayList<Object> resources = new ArrayList<Object>();
/**
* List of configuration parameters marked <b>final</b>.
* finalParameters集合中保留的是final修饰的不可变的参数
*/
private Set<String> finalParameters = new HashSet<String>();
/**
* 是否加载默认资源配置
*/
private boolean loadDefaults = true;
/**
* Configuration objects
* Configuration对象
*/
private static final WeakHashMap<Configuration,Object> REGISTRY =
new WeakHashMap<Configuration,Object>();
/**
* List of default Resources. Resources are loaded in the order of the list
* entries
*/
private static final CopyOnWriteArrayList<String> defaultResources =
new CopyOnWriteArrayList<String>();
上面只是列举出了一部分,基本的用途都是拿来保存一些资源的数据。还有一个变量比较关键:
//资源配置文件中的属性会加载到Properties属性中来
private Properties properties;
所有的属性变量都是存放到java中的Properties中存放,便于后面的直接存取。Property其实就是一个HashTable。我们按着Configuration加载的顺序来学习一下他的整个过程。首先当然是执行初始化代码块:
static{
//print deprecation warning if hadoop-site.xml is found in classpath
ClassLoader cL = Thread.currentThread().getContextClassLoader();
if (cL == null) {
cL = Configuration.class.getClassLoader();
}
if(cL.getResource("hadoop-site.xml")!=null) {
LOG.warn("DEPRECATED: hadoop-site.xml found in the classpath. " +
"Usage of hadoop-site.xml is deprecated. Instead use core-site.xml, "
+ "mapred-site.xml and hdfs-site.xml to override properties of " +
"core-default.xml, mapred-default.xml and hdfs-default.xml " +
"respectively");
}
//初始化中加载默认配置文件,core-site是用户的属性定义
//如果有相同,后者的属性会覆盖前者的属性
addDefaultResource("core-default.xml");
addDefaultResource("core-site.xml");
}
学习过java构造函数的执行顺序的同学,应该知道初始化代码块中的代码的执行顺序是先于构造函数的,所以会执行完上面的操作,就来到了addDefaultResource():
/**
* Add a default resource. Resources are loaded in the order of the resources
* added.
* @param name file name. File should be present in the classpath.
*/
public static synchronized void addDefaultResource(String name) {
if(!defaultResources.contains(name)) {
defaultResources.add(name);
//遍历注册过的资源配置,进行重新加载操作
for(Configuration conf : REGISTRY.keySet()) {
if(conf.loadDefaults) {
conf.reloadConfiguration();
}
}
}
}
把资源的名字加入到相应的集合中,然后遍历每个配置类,重新加载配置操作,因为默认资源列表改动了,所以要重新加载了,这个也好理解。这里简单介绍一下,每一个Configuration类初始化后,都会加入到REGISTRY集合中,这是一个static 变量,所以会保持全局统一的一个。然后把重点移到reloadConfiguration():
/**
* Reload configuration from previously added resources.
*
* This method will clear all the configuration read from the added
* resources, and final parameters. This will make the resources to
* be read again before accessing the values. Values that are added
* via set methods will overlay values read from the resources.
*/
public synchronized void reloadConfiguration() {
//重新加载Configuration就是重新将里面的属性记录清空
properties = null; // trigger reload
finalParameters.clear(); // clear site-limits
}
操作非常简单,就是clear一些操作,也许这时候,你会想难道不用马上加载新的资源吗?其实这也是作者的一大设计,答案在后面。好的,程序执行到这里,初始化代码块的操作完成了,接下来就是构造函数的执行了:
/** A new configuration. */
public Configuration() {
//初始化是需要加载默认资源的
this(true);
}
然后继续调用重载函数:
/** A new configuration where the behavior of reading from the default
* resources can be turned off.
*
* If the parameter {@code loadDefaults} is false, the new instance
* will not load resources from the default files.
* @param loadDefaults specifies whether to load from the default files
*/
public Configuration(boolean loadDefaults) {
this.loadDefaults = loadDefaults;
if (LOG.isDebugEnabled()) {
LOG.debug(StringUtils.stringifyException(new IOException("config()")));
}
synchronized(Configuration.class) {
//加载过的Configuration对象对会加入到REGISTRY集合中
REGISTRY.put(this, null);
}
this.storeResource = false;
}
重点观察人家把当前初始化的Configuration类加入到全局REGISTRY里面了。
以上分析的代码都是前期的操作,那么比较关键的set/get这类和属性直接相关的方法怎么实现的,所以这个时候,必须要先了解Hadoop中的配置文件是怎样的格式存在于文件中的。比如HDFS的配置文件hdfs-site.xml;
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>
<!-- file system properties -->
<property>
<name>dfs.name.dir</name>
<value>/var/local/hadoop/hdfs/name</value>
<description>Determines where on the local filesystem the DFS name node
should store the name table. If this is a comma-delimited list
of directories then the name table is replicated in all of the
directories, for redundancy. </description>
<final>true</final>
</property>
<property>
<name>dfs.data.dir</name>
<value>/var/local/hadoop/hdfs/data</value>
<description>Determines where on the local filesystem an DFS data node
should store its blocks. If this is a comma-delimited
list of directories, then data will be stored in all named
directories, typically on different devices.
Directories that do not exist are ignored.
</description>
<final>true</final>
</property>
.......
节点层级的关系不是很复杂,关键在每个Property节点保留name名字,value值,des对于此属性的描述,final标签用于判断此属性能不能被改变,为true代表无法变更,类似于java语言里的final关键字。了解完配置文件的结构之后,就可以继续往下看了,比如我要设置1个属性,set的一个小小的方法如下:
/**
* Set the <code>value</code> of the <code>name</code> property.
*
* @param name property name.
* @param value property value.
* 根据name设置属性值,属性键值对保存在property中
*/
public void set(String name, String value) {
getOverlay().setProperty(name, value);
getProps().setProperty(name, value);
}
后面的setProperty就是Property的设置方法,jdk的API,所以关键就是前面获取getProps的方法,如何把文件中的属性加载到Property的变量中的。/**
* 加载的时候采用了延时加载的策略
* @return
*/
private synchronized Properties getProps() {
if (properties == null) {
properties = new Properties();
//从资源中再次获取属性相关的数据
loadResources(properties, resources, quietmode);
if (overlay!= null) {
properties.putAll(overlay);
if (storeResource) {
for (Map.Entry<Object,Object> item: overlay.entrySet()) {
updatingResource.put((String) item.getKey(), "Unknown");
}
}
}
}
return properties;
}
看了上面为NULL的判断,也许你就知道为什么刚刚的重新加载操作那么简单,就执行了clear操作就完了,就是等着后面真正要获取这个Property的时候在加载的,就是所谓的延时加载策略,类似于单例模式中的懒汉式模型。所以loadResources又是此实现的关键:
private void loadResource(Properties properties, Object name, boolean quiet) {
try {
//工厂模式获取解析xml文件对象,这里用的是doc解析方式
DocumentBuilderFactory docBuilderFactory
= DocumentBuilderFactory.newInstance();
//ignore all comments inside the xml file
docBuilderFactory.setIgnoringComments(true);
//allow includes in the xml file
docBuilderFactory.setNamespaceAware(true);
try {
docBuilderFactory.setXIncludeAware(true);
} catch (UnsupportedOperationException e) {
LOG.error("Failed to set setXIncludeAware(true) for parser "
+ docBuilderFactory
+ ":" + e,
e);
}
DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
.....
if (root == null) {
//获取xml中的节点进行获取,这里先获取了根节点
root = doc.getDocumentElement();
}
if (!"configuration".equals(root.getTagName()))
LOG.fatal("bad conf file: top-level element not <configuration>");
NodeList props = root.getChildNodes();
for (int i = 0; i < props.getLength(); i++) {
Node propNode = props.item(i);
if (!(propNode instanceof Element))
continue;
Element prop = (Element)propNode;
if ("configuration".equals(prop.getTagName())) {
//如果子节点是configuration,则再次递归调用loadResource()方法
loadResource(properties, prop, quiet);
continue;
}
if (!"property".equals(prop.getTagName()))
LOG.warn("bad conf file: element not <property>");
NodeList fields = prop.getChildNodes();
String attr = null;
String value = null;
boolean finalParameter = false;
for (int j = 0; j < fields.getLength(); j++) {
Node fieldNode = fields.item(j);
if (!(fieldNode instanceof Element))
continue;
//属性节点分3种判断,name,value,final
Element field = (Element)fieldNode;
if ("name".equals(field.getTagName()) && field.hasChildNodes())
attr = ((Text)field.getFirstChild()).getData().trim();
if ("value".equals(field.getTagName()) && field.hasChildNodes())
value = ((Text)field.getFirstChild()).getData();
if ("final".equals(field.getTagName()) && field.hasChildNodes())
//final参数需额外添加到finalParameters参数的集合中
finalParameter = "true".equals(((Text)field.getFirstChild()).getData());
}
// Ignore this parameter if it has already been marked as 'final'
if (attr != null) {
if (value != null) {
if (!finalParameters.contains(attr)) {
//在这步把上面去的值放入properties属性中
properties.setProperty(attr, value);
if (storeResource) {
updatingResource.put(attr, name.toString());
}
} else if (!value.equals(properties.getProperty(attr))) {
LOG.warn(name+":a attempt to override final parameter: "+attr
+"; Ignoring.");
}
}
if (finalParameter) {
finalParameters.add(attr);
}
}
}
和上面我们看的实际配置文件一对照,就不难理解了,就是简单的doc解析xml文件,这里不过多了一些处理,比如final的参数要额外再做一下操作。加载完成之后,属性信息就被放到了Property中了,就达成了目标了。
下面我们说说get的属性获取操作,同样有别样的设计,他可不仅仅是getProps().get(name)这样的操作,因为有的时候,通过这样的操作还无法取出真正想要的值。比如下面这样的结构:
<property>
<name>dfs.secondary.namenode.kerberos.principal</name>
<value>hdfs/_HOST@${local.realm}</value>
<description>
Kerberos principal name for the secondary NameNode.
</description>
</property>
也许你会直接通过dfs.secondary.namenode.kerberos.principal这个name去获取值,然后获取的值就是hdfs/_HOST@${local.realm},但是很显然这不是我们需要的值,因为中间还有${local.realm},这个其实代表的是另外的一个设置的值,有的时候更多的是系统变量的值,所以这一点告诉我们,在值的查找操作里面我们需要替换这些变量。
/**
* Get the value of the <code>name</code> property, <code>null</code> if
* no such property exists.
*
* Values are processed for <a href="#VariableExpansion">variable expansion</a>
* before being returned.
*
* @param name the property name.
* @return the value of the <code>name</code> property,
* or null if no such property exists.
*/
public String get(String name) {
return substituteVars(getProps().getProperty(name));
}
所以Hadoop在获取值后又进行了一步值替换的操作,用到了正则表达式。
//需匹配的模式为\$\{[^\}\$ ]+\},里面多的\是在java里进行转义
//$,{,}是正则表达式中的保留字,因此需要加\,此匹配可分解为
//'\$\'{匹配的是${的部分
//最后的'\}'匹配了结尾符},这样就构成了初步的${....}的目标类型结构了
//中间[^\}\$ ]匹配了除了},$,空格除外的关键字
//+是1个修饰次数,保证中间的匹配至少为1次,也就是说中间至少有值存在
private static Pattern varPat = Pattern.compile("\\$\\{[^\\}\\$\u0020]+\\}");
private static int MAX_SUBST = 20;
private String substituteVars(String expr) {
//输入的属性匹配值,为空的话直接返回
if (expr == null) {
return null;
}
Matcher match = varPat.matcher("");
String eval = expr;
//避免循环迭代陷入死循环,这里强制最多MAX_SUBST20次的替换
for(int s=0; s<MAX_SUBST; s++) {
match.reset(eval);
//寻找模式是否匹配
if (!match.find()) {
return eval;
}
String var = match.group();
//找到之后去掉头尾的${,和},截取出中间部分
var = var.substring(2, var.length()-1); // remove ${ .. }
String val = null;
try {
//看看此属性是否为系统变量
val = System.getProperty(var);
} catch(SecurityException se) {
LOG.warn("Unexpected SecurityException in Configuration", se);
}
if (val == null) {
val = getRaw(var);
}
if (val == null) {
return eval; // return literal ${var}: var is unbound
}
// substitute
//然后取出对应的值进行替换,再次查找是否有${..}类型值的存在
eval = eval.substring(0, match.start())+val+eval.substring(match.end());
}
throw new IllegalStateException("Variable substitution depth too large: "
+ MAX_SUBST + " " + expr);
}
关键的难点是对于${...}这种模式的匹配器的构造,像我这种平时对于正则表达式第一想到的是上网找的人来说,就比较难想到了。还有1个特殊处理就是为了避免替换之后还会存在${...}出现死循环,所以这里有次数的限制。get操作的实现就是如此。最后看看我对于Configuration做的2条不同情况下的流程分析图:
配置类代码的实现应该说是短小和精炼,以后开发大型系统的时候完全可以借鉴此类似的原理。