• Hadoop Configuration配置类的分析


              学习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条不同情况下的流程分析图:


    配置类代码的实现应该说是短小和精炼,以后开发大型系统的时候完全可以借鉴此类似的原理。

  • 相关阅读:
    c++获取线程id
    一个基于c++的log库
    防止socket程序重启等待2MSL时间
    c++头文件循环引用
    Myeclipse 8.5 优化设置
    来道题 求解释
    MyEclipse常用设置笔记
    Ubuntu 学习笔记
    Linux 下常用命令
    Oracle 学习笔记 常用查询命令篇
  • 原文地址:https://www.cnblogs.com/bianqi/p/12184142.html
Copyright © 2020-2023  润新知