TechReset

最近研究Cloud Foundry与Openstack的集成功能,考虑到最终运用需要一个可支持动态部署的环境,因此选择了使用VMWare推荐的BOSH进行持续部署, 同时由于BOSH相关的文档非常有限,对于各种配置项的含义并没有专业的文档进行介绍,本着知其然要知其所以然的精神,对BOSH的代码进行了一些研究。 同时作为Ruby新手,在阅读中被Ruby动态编程特性所折服,学习了很多Ruby元编程的奇淫技巧,在此总结一二。

本文中多处比较ruby和java的实现差异,是为突出动态语言在处理一类问题的思考方式与静态语言的差异,而非比较语言本身优劣。特此声明,以免陷入无意义的语言之争。

class.send(:method,args)

静态语言如Java如果想动态的调用一个类的一个方法,大概得这样写:

Class myClass=Class.forName("package.to.MyClass");
Object classInstance=myClass.newInstance();
Method theMethod=myClass.getMethod("methodName", new Class[]{});
theMethod.invoke();
...

作为动态语言的标志性特性,Ruby可以用class.send(:method,args)方法调用类的一切方法,同时配合class.method_missing(method_name, *args)方法使用,淋漓尽致地展现了动态语言的优美之处。一个例子的运用在于agent_client的代码实现中。

module Bosh
  module Agent
    class BaseClient

      def run_task(method, *args)
        task = send(method.to_sym, *args)
        ...
        task
      end

      def method_missing(method_name, *args)
        result = handle_method(method_name, args)

        raise HandlerError, result["exception"] if result.has_key?("exception")
        result["value"]
      end

      protected
      def handle_method(method_name, args)
      end
    end
  end
end

在部署过程中deployer会调用agent_client与stemcell中的agent通信,命令agent做部署等其他操作,因此在deployer方大多就调用HTTPClient继承 自BaseClient的run_task方法,然而由于大多数的请求都差不多,都是post_json类型的请求,这里就通过send方法加上method_misstiong方法来处理各种请求,当某个请求在开发过程中需要加入特殊行为时,再行加入该方法即可,降低了deployer与agent_client之间的耦合。

这是一个非常基本的特性,这里列出只是为了在未来编程中更好地运用该特性

method_added(name)

我的理解是method_added(name)是一种钩子方法,当module的继承类在定义方法后,ruby解释器会触发method_added事件,从而调用该方法做一些自定义的操作。 stackoverflow上有一个问题的回答,也举例说明了这个问题。

module Magic
	def	self.included(base)
	    base.extend ClassMethods
	end

    module ClassMethods
        def method_added(name)
              puts "instance method '#{name}' added"
        end

	    def singleton_method_added(name)
	          puts "class method '#{name}' added"
	    end
	end
end

classFoo
	include Magic

	def bla
	end

	def blubb
	end

	def self.foobar
	end
end

Output

instance method 'bla' added
instance method 'blubb' added
class method 'foobar' added

有了这个简单的例子,我们来看看bosh_cli是如何实现响应用户的命令请求输入的

入口在bosh_cli/bin/bosh,其中就是调用Bosh::Cli::Runner.run(ARGV.dup)来响应。 在Bosh::Cli::Runner.run(ARGV.dup)中,代码如下:

class Runner
	def self.run(args)
      new(args).run
    end

    # @param [Array] args
    def initialize(args, options = {})
      @args = args
      @options = options.dup

      banner = "Usage: bosh [<options>] <command> [<args>]"
      @option_parser = OptionParser.new(banner)

      Config.colorize = true
      Config.output ||= STDOUT
    end

    # Find and run CLI command
    # @return [void]
    def run
    	...
      load_plugins
      build_parse_tree
		...

      command = search_parse_tree(@parse_tree)

      ...

      command.runner = self
      begin
        exit_code = command.run(@args, @options)
        exit(exit_code)
      rescue OptionParser::ParseError => e
        ...
     end

    def build_parse_tree
      @parse_tree = ParseTreeNode.new

      Config.commands.each_value do |command|
        p = @parse_tree
        n_kw = command.keywords.size

        command.keywords.each_with_index do |kw, i|
          p[kw] ||= ParseTreeNode.new
          p = p[kw]
          p.command = command if i == n_kw - 1
        end
      end
    end
    ...
end

看起来一切正常,静态方法run创建一个instance,然后调用instance的run方法,先建立命令处理的tree然后从中搜索输入的方法,然后run该方法。这里build_parse_tree中根据Config.commands中的value轮询一遍,建立树结构。但是当我去看Config.commands的时候,发现

@commands = {}

是空的啊,程序顺序执行的话,这里还没什么方法往@commands里加K-V呢。那么程序是在什么时候往这个map结构种加入值的呢?只可能是类加载的时候。 从包和文件的命名规则来看,lib/cli/commands/*.rb肯定是最终处理请求的各种命令,且都继承自Bosh::Cli::Command::Base,而后者扩展了模块 Bosh::Cli::CommandDiscovery. Bosh::Cli::Command::Base并没有什么特别的,定义了一些基本的操作,所有秘密都在Bosh::Cli::CommandDiscovery里:

module Bosh::Cli
  module CommandDiscovery

    def usage(string = nil)
      @usage = string
    end

    def desc(string)
      @desc = string
    end

    def option(name, *args)
      (@options ||= []) << [name, args]
    end

    # @param [Symbol] method_name Method name
    def method_added(method_name)
      if @usage && @desc
        @options ||= []
        method = instance_method(method_name)
        register_command(method, @usage, @desc, @options)
      end
      @usage = nil
      @desc = nil
      @options = []
    end

    # @param [UnboundMethod] method Method implementing the command
    # @param [String] usage Command usage (used to parse command)
    # @param [String] desc Command description
    # @param [Array] options Command options
    def register_command(method, usage, desc, options = [])
      command = CommandHandler.new(self, method, usage, desc, options)
      Bosh::Cli::Config.register_command(command)
    end

  end
end

定义了三个常量,但是有一个特殊的method_added方法,这下就全清楚了,在类加载的时候,当加入新的命令行处理方法时, 都会调用该方法搜集命令行处理方法,然后通过register_command将方法转换为CommandHandler的实例,然后调用Bosh::Cli::Config.register_commandBosh::Cli::Config的@commands中注册。事实也确实如此,看Bosh::Cli::Config.register_command的代码如下

def self.register_command(command)
      if @commands.has_key?(command.usage)
        raise CliError, "Duplicate command `#{command.usage}'"
      end
      @commands[command.usage] = command
end

这样,任何一个想要扩展bosh命令从命令行执行的方法,只需要继承Bosh::Cli::Command::Base并加入一组处理方法就可以自动地向支撑的bosh所有命令行方法种注册了,示例如下

usage "init release"
desc "Initialize release directory"
option "--git", "initialize git repository"
def init(base = nil)
  ...
end

这些用法/描述/选项和直接处理的方法,都被转换成一个Bosh::Cli:CommandHandler对象

def initialize(klass, method, usage, desc, options = [])
  @klass = klass
  @method = method
  @usage = usage
  @desc = desc

  @options = options

  @hints = []
  @keywords = []

  @parser = OptionParser.new
  @runner = nil
  extract_keywords
end

# Run handler with provided args
# @param [Array] args
# @return [Integer] Command exit code
def run(args, extra_options = {})
  handler = @klass.new(@runner)

  @options.each do |(name, arguments)|
    @parser.on(name, *arguments) do |value|
      handler.add_option(format_option_name(name), value)
    end
  end

  extra_options.each_pair do |name, value|
    handler.add_option(format_option_name(name), value)
  end

  args = parse_options(args)

  begin
    handler.send(@method.name, *args)
    handler.exit_code
  rescue ArgumentError => e
    say(e.message)
    err("Usage: #{usage_with_params}")
  end
end

综上所述,如果需要扩展BOSH Cli的操作,只需要继承Bosh::Cli::Command::Base,依次调用usage desc option并定义实现方法,该方法就会在类加载的过程中自动被包装成Bosh::Cli:CommandHandler对象,并且以 K->usage V->CommandHandler的方式注册到Bosh::Cli::Config的@commands变量中,最终在执行时,调用包装后的CommandHandler实例的run方法,新建所定义的新命令行操作类的实例, 并调用相应的方法执行请求。事实上,bosh_cli_plugin_awsbosh_cli_plugin_micro正是如此,将方法自动加入到Bosh命令行中,避免了对Bosh_cli代码的影响,方便扩展。

如果此类方法用java实现,可以通过反射机制加载所需要的对象,然后拼出对应的方法来执行,然而灵活性远比动态语言差。或许还有其他更好的方法

#END