Common源码阅读---Configuration

Hadoop版本:Hadoop-1.2.1
参考:《Hadoop技术内幕-深入解析Hadoop Common和HDFS架构设计与实现原理》,后文简称技术内幕


Hadoop使用Configuration管理配置文件,使用Configuration一般过程为:
构造Configuration对象,通过addResource方法添加需要加载的资源(配置文件),之后便通过get*方法和set*方法访问/设置配置项。

通过addResource先后加载不同配置文件时,如果不同配置文件中有相同的配置项,且该配置项在先加载的配置文件中没有配置成final,则后加载的配置文件中配置项会覆盖先加载的配置项。
Configuration中,每个属性都是String类型的,但是值类型可能有多个类型。如boolean(getBoolean),int(getInt),long(getLong),float(getFloat), String(get),java.io.File(getFile),String数组(getStrings)等。因此get*set*对不同的类型有不同的后缀。

1. 成员

1
2
3
4
5
6
7
8
9
private boolean quietmode = true;
private ArrayList<Object> resources = new ArrayList<Object>();
private Set<String> finalParameters = new HashSet<String>();
private boolean loadDefaults = true;
private static final CopyOnWriteArrayList<String> defaultResources = new CopyOnWriteArrayList<String>();
private HashMap<String, String> updatingResource;
private Properties properties;
private Properties overlay;
private static final WeakHashMap<Configuration,Object> REGISTRY = new WeakHashMap<Configuration,Object>();

quitemode为true时,在加载配置文件的过程中,不输出日志信息,主要用于调试。
resources所有通过addResource添加的资源,不包括default资源
finalParameters所有final属性为true的键
loadDefaults是否决定加载默认资源,通过addDefaultResource加载
defaultResources静态成员,默认资源,所有配置共享默认资源
updatingResource最近加载或修改的键-资源映射
properties配置文件解析后的键-值
REGISTRY静态成员,存储已创建的Configuration-null

2. 创建

默认构造方式

1
2
3
4
5
6
7
8
9
10
public Configuration() {
this(true);
}//缺省loadDefaults为true
public Configuration(boolean loadDefaults) {
this.loadDefaults = loadDefaults;
updatingResource = new HashMap<String, String>();
synchronized(Configuration.class) {
REGISTRY.put(this, null);
}
}//可以关闭loadDefaults。创建完后在REGISTRY中注册

从其他的Configuration创建新的Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Configuration(Configuration other) {
this.resources = (ArrayList) other.resources.clone();//克隆resources
synchronized (other) {
if (other.properties != null) {
this.properties = (Properties) other.properties.clone();//克隆所有键值对
}
if (other.overlay != null) {
this.overlay = (Properties) other.overlay.clone();//克隆overlay
}
this.updatingResource = new HashMap<String, String>(other.updatingResource);
}

this.finalParameters = new HashSet<String>(other.finalParameters);
synchronized (Configuration.class) {
REGISTRY.put(this, null);
}//注册
}

将其他的Configuration的配置信息克隆过来。

3. 资源加载

通过addResource或者addDefaultResource加载,addResource更新resources成员,addDefaultResources更新defaultResources成员。

3.1 addResource

有多种形式,不同的资源来源

1
2
3
4
5
6
7
8
9
10
11
12
public void addResource(String name) {
addResourceObject(name);
}//资源为classpath中的文件名name
public void addResource(URL url) {
addResourceObject(url);
}//资源名为URL形式
public void addResource(Path file) {
addResourceObject(file);
}//资源名为HDFS中的path路径形式
public void addResource(InputStream in) {
addResourceObject(in);
}//从输入流中反序列化

可以通过String,URL,Path,InputStream等方式提供资源,最终都是通过addResourceObject加载资源

1
2
3
4
5
6
7
8
private synchronized void addResourceObject(Object resource) {
resources.add(resource); // add to resources
reloadConfiguration();//之后会加载resources中所有资源
}
public synchronized void reloadConfiguration() {
properties = null; // trigger reload
finalParameters.clear(); // clear site-limits
}

将资源添加到resources中,然后通过将properties置null,清除finalParameters,这将在get*set*时触发资源的重新加载。

3.2 addDefaultResource

1
2
3
4
5
6
7
8
9
10
public static synchronized void addDefaultResource(String name) {
if(!defaultResources.contains(name)) {
defaultResources.add(name);
for(Configuration conf : REGISTRY.keySet()) {
if(conf.loadDefaults) {
conf.reloadConfiguration();
}
}
}
}

默认资源保存在defaultResources中,如果要添加的name没有在其中,则加到defaultResource中,然后从REGISTRY中读取所有的Configuration对象,如果该对象允许加载默认资源,则重新加载配置文件。(defaultResource为所有Configuration共享,因此要对所有配置对象进行处理)

3.3 reloadConfiguration重新加载配置资源

所有的get和set方法都会使用getProps方法获取当前的配置键值对信息properties,然后获取相应键的值或设置键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
private synchronized Properties getProps() {
if (properties == null) {//properties为null,重新加载resources中的所有资源
properties = new Properties();
loadResources(properties, resources, quietmode);//加载资源到properties中
if (overlay!= null) {
properties.putAll(overlay);
for (Map.Entry<Object,Object> item: overlay.entrySet()) {
updatingResource.put((String) item.getKey(), UNKNOWN_RESOURCE);
}
}
}
return properties;
}

因此,调用reloadConfiguration后,采用了延迟加载的方式,真正需要配置数据的时候,才会在set或get中重新加载resources所有的资源,

loadResources如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void loadResources(Properties properties, ArrayList resources,boolean quiet) {
if(loadDefaults) {
for (String resource : defaultResources) {//defaultResources中的缺省资源,通过addDefaultResource更新
loadResource(properties, resource, quiet);
}
//support the hadoop-site.xml as a deprecated case
if(getResource("hadoop-site.xml")!=null) {//这里hadoop-site.xml为classpath中的文件
loadResource(properties, "hadoop-site.xml", quiet);
}
}

for (Object resource : resources) {
loadResource(properties, resource, quiet);
}
}

因此,如果需要加载缺省资源,则加载通过addDefaultResource添加的缺省资源,以及classpath中的hadoop-site.xml文件,然后加载resources。
loadResource中使用的便是JAVA中典型的解析XML的文档对象模型(DOM)方式,将XML文档一次性加载到内存中解析。

3.4 DOM解析

首先是DOM解析器的构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void loadResource(Properties properties, Object name, boolean quiet) {
try {
//创建DOM解析器的工厂
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
//ignore all comments inside the xml file
docBuilderFactory.setIgnoringComments(true);

//allow includes in the xml file
docBuilderFactory.setNamespaceAware(true);
try {
//允许XInclude机制
docBuilderFactory.setXIncludeAware(true);
} catch (UnsupportedOperationException e) {
...
}
DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
...

从构建DOM解析器工厂开始,工厂构建完后设置,包括忽略注释,允许XML名字空间,以及允许XInclude机制。

XInclude机制允许将XML文档分解为多个可管理的块,然后将一个或多个较小的文档组装成一个大型文档,即可以利用XInclude机制将其他配置文件包含进来处理,如:

1
2
3
4
5
<configuration xmlns:xi="http://www.w3.org/2001/XInclude">
...
<xi:include href="conf4performance.xml"/>
...
</configuration>

通过XInclude机制,把配置文件conf4performance.xml嵌入到当前配置文件,这种方法更有利于对配置文件进行模块化管理,同时就不需要再使用Configuration.addResource方法加载资源conf4performance.xml了。

设置好DOM解析器工厂后,创建解析器对象DocumentBuilder,用于解析不同的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Document doc = null;
Element root = null;

if (name instanceof URL) { // an URL resource
URL url = (URL)name;
if (url != null) {
if (!quiet) {
LOG.info("parsing " + url);
}
doc = builder.parse(url.toString());
}
} else if (name instanceof String) { // a CLASSPATH resource
URL url = getResource((String)name);
if (url != null) {
if (!quiet) {
LOG.info("parsing " + url);
}
doc = builder.parse(url.toString());
}
} else if (name instanceof Path) { // a file resource
// Can't use FileSystem API or we get an infinite loop
// since FileSystem uses Configuration API. Use java.io.File instead.
File file = new File(((Path)name).toUri().getPath()).getAbsoluteFile();
if (file.exists()) {
if (!quiet) {
LOG.info("parsing " + file);
}
InputStream in = new BufferedInputStream(new FileInputStream(file));
try {
doc = builder.parse(in);
} finally {
in.close();
}
}
} else if (name instanceof InputStream) {
try {
doc = builder.parse((InputStream)name);
} finally {
((InputStream)name).close();
}
} else if (name instanceof Element) {//configuration子元素
root = (Element)name;
}

if (doc == null && root == null) {
if (quiet)
return;
throw new RuntimeException(name + " not found");
}

对不同的资源调用parse进行解析,即所说的读取整个文档到内存中进行解析。
对于最后一种name是一个元素的,是为了处理文档中configuration元素中包含configuration子元素的情况。

解析后便是对解析结果进行处理,填充properties,finalParameters,updatingResource等相关成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
if (root == null) {
root = doc.getDocumentElement();
}
if (!"configuration".equals(root.getTagName()))//文档的根元素应该是configuration
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;
//configuration子元素,递归调用loadResource进行处理,对应上面name参数为Element的情况
if ("configuration".equals(prop.getTagName())) {
loadResource(properties, prop, quiet);
continue;
}
if (!"property".equals(prop.getTagName()))//configuration有效元素为property,对应properties中一个属性
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;
Element field = (Element)fieldNode;
if ("name".equals(field.getTagName()) && field.hasChildNodes())//name
attr = ((Text)field.getFirstChild()).getData().trim();
if ("value".equals(field.getTagName()) && field.hasChildNodes())//value
value = ((Text)field.getFirstChild()).getData();
if ("final".equals(field.getTagName()) && field.hasChildNodes())//final属性
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)) {//没被设置为final属性,可以添加或覆盖
properties.setProperty(attr, value);//更新properties
updatingResource.put(attr, name.toString());//更新updatingResource
} else if (!value.equals(properties.getProperty(attr))) {
LOG.warn(name+":a attempt to override final parameter: "+attr
+"; Ignoring.");
}
}
if (finalParameter) {
finalParameters.add(attr);//更新finalParameters
}
}
}

如上,对于每一个property元素,对应为properties中一个值。查找property元素中的name,value,finale子元素,添加至相应成员中。

3.5 属性访问

Configuration中包含很多get和set方法,用于获取和设置相应的属性。
get可以获取不同类型的属性,包括boolean(getBoolean),int(getInt),long(getLong)等基本类型,也可以是其他Hadoop常用类型,如类的信息(getClassByName,getClasses,getClass等),String数组(getStringCollection,getStrings),URL等。这些方法都会依赖于get,然后在get的基础上做进一步处理。

1
2
3
public String get(String name) {
return substituteVars(getProps().getProperty(name));
}

getProps在上面已经分析过了,获取properties成员,若为null需重新加载资源。
使用substituteVars对获取到的属性进行扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
for(int s=0; s<MAX_SUBST; s++) {//最多迭代20次
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);//系统属性中不存在,获取properties中的属性
}
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);
}

如上,属性扩展可以迭代,即${key1}扩展出来的属性仍然可以扩展的话,需要继续扩展。不过为了防止出现${key1}扩展结果包含属性${key2},而${key2}扩展包含${key1}的情况进入死循环,最多迭代20次,超出时抛出异常。
正则表达式为${[^}$ ]+},即匹配${…}这种情况,中间的…表示除$,}和空格之外的其他任何符号。
属性扩展时,优先从系统属性(如user.home)中查找,Java命令行可通过”-D=“的方式来定义系统属性,系统属性不存在时,查找目前properties中已经定义了的属性,两者都没找到的话该属性没有定义,扩展失败。getRaw(var)如下:

1
2
3
public String getRaw(String name) {
return getProps().getProperty(name);
}//从properties中查找相应属性

每次属性扩展后,将找到的部分替换原来的${…},继续扩展,没有匹配正则表达式,表示不存在${…},扩展完成,返回。

与get类似,set系列的方法大多依赖set方法,如下

1
2
3
4
5
public void set(String name, String value) {
getOverlay().setProperty(name, value);
getProps().setProperty(name, value);
this.updatingResource.put(name, UNKNOWN_RESOURCE);
}

设置两个Property对象中的键值对,updatingResource中对应的资源为UNKNOWN_RESOURCE,为字符串”Unknown”。

4. Configurable接口

包含两个方法,如果一个类实现了Configurable接口,表示这个类可以配置,即可以通过为这个类的对象传入一个Configuration实例,提供对象工作需要的一些配置信息。

1
2
3
4
5
6
7
8
public interface Configurable {

/** Set the configuration to be used by this object. */
void setConf(Configuration conf);

/** Return the configuration used by this object. */
Configuration getConf();
}

一般来讲,对于实现了Configurable接口的类,在创建对象时便可调用setConf方法设置Configuration实例。