论坛首页 编程语言技术论坛

Ruby On Rails-2.0.2源代码分析(4)-寻找Controller

浏览 7473 次
该帖已经被评为良好帖
作者 正文
   发表时间:2008-03-24  
  • 前言

  经过一番试验和考虑...一,我尝试了一些思维导图工具(MindMapper,FREEMIND),但我始终没有找到一种好的方式将自己学习Rails源代码的思路表述出来,就此作罢(顺便问问,有研究思维导图的同学么?能否推荐两个自己觉得用起来比较顺手的工具)。二,不再打算整理代码运行顺序图,对不熟悉Rails源代码的同学们来说,这个图可能的确没什么帮助,甚至会把人搞晕。我现在打算从Rails源代码功能点的角度出发,根据具体功能点,结合Rails源代码进行学习,整理,总结。如果某些源代码比较复杂,牵涉类比较繁多,我仍然打算整理一个类图,从一个高的层次了解系统内部对象的关系。
  前面三篇文章,我们看到了Rails启动的大致功能和流程,包括初始化多种环境变量,初始化Route表,启动Web服务器开始侦听客户端请求。。。那么接下来,当然是开门迎客,等待客户端(浏览器)的请求,并进行处理,最终将结果返回客户端(浏览器)呈现。那么熟悉Rails的同学都知道,首先,Rails必须根据客户端的一个请求,决定将要执行哪个Controller的哪个Action,这也是本文的主要目的。

  • 寻找Controller

首先,我们先来看一看Rails通过客户端请求,查找Controller的大致流程图

  (一)生成DisapatchServlet实例,开始服务吧
  源代码:gems/rails-2.0.2/lib/webrick_server.rb
  在第一篇文章,讲解Rails的启动时,我提到webrick_server.rb中定义了DisapatchServlet类,此类启动了WEBrick,开始侦听客户端请求。当有客户端请求到达时,会生成一个DispatchServlet实例,具体代码如下:

 

class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet

  def initialize(server, options) #:nodoc:
    @server_options = options
    @file_handler = WEBrick::HTTPServlet::FileHandler.new(server, options[:server_root])
    # Change to the RAILS_ROOT, since Webrick::Daemon.start does a Dir::cwd("/")
    # OPTIONS['working_directory'] is an absolute path of the RAILS_ROOT, set in railties/lib/commands/servers/webrick.rb
    Dir.chdir(OPTIONS['working_directory']) if defined?(OPTIONS) && File.directory?(OPTIONS['working_directory'])
    super
  end
  ...
end


  初始化参数server是web服务器的类型,当然,在我的环境中是WEBRick::HTTPServer。option是一个hash,包含了一些列的环境参数,这里,我将一些比较重要的参数罗列出来:

名称 类型 参考值
port Fixnum 3000
ip String 0.0.0.0(因为我是本机操作)
environment String development
charset String UTF-8
working_directory String D:\Project\Ruby\blog

  初始化中,首先将option参数赋DispatchServlet的@server_options变量,然后生成一个FileHandler对象,这个对象的具体作用马上会提到。紧接着将Rails的工作目录设置为“working_directory”,也就是前面文章提到过的RAIL_ROOT。至此,DsipatchServlet的初始化工作完成了。WEBRick会执行此Servlet的service方法。

  (二)是否存在相应html
  源代码:gems/rails-2.0.2/lib/webrick_server.rb
  第一步生成了Servlet实例,并且,开始执行service方法,我们先来看看service方法的具体内容:

 

class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet

  def service(req, res) #:nodoc:
    unless handle_file(req, res)
      begin
        REQUEST_MUTEX.lock unless ActionController::Base.allow_concurrency
        unless handle_dispatch(req, res)
          raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
        end
      ensure
        unless ActionController::Base.allow_concurrency
          REQUEST_MUTEX.unlock if REQUEST_MUTEX.locked?
        end
      end
    end
  end
  ...
end


  此方法算是处理一个Request的最高层次描述,首先是方法handle_file。这个方法会使用初始化生成的FileHandler对象,查找针对客户端请求的path,在RAILS_ROOT/public目录下是否存在相应的html。例如客户端的请求是http://localhost:3000/posts ,那么首先Rails就使用FileHandler查找在public根目录下面是否存在posts.html,如果存在的话,则直接向客户端呈现这个html,如果不存在,OK,开始寻找Controller吧。
  (这里,值得一提是并发控制,默认情况下,Rails只允许一次dispatch一个request,当然,我们可以通过在程序配置文件中设置ActionController::Base.allow_concurrency来改变这个默认的行为。)
  (我想你应该知道很多Rails书籍提到过,如果你在routes.rb中通过map.root :controller=>'posts'的方式,使得当用户通过http://www.yoursite.com 访问站点时,显示相应的功能页面。但是你必须把public下的index.html删除掉,就是这个原因。)
  (handle_file源代码不列出,因为他十分简单,只是调用FileHandler的相应方法,而WEBRick暂不在研究范围内。)

  (三)开始Dispatch吧
  源代码:gems/rails-2.0.2/lib/webrick_server.rb
              gems/actionpack-2.0.2/lib/action_controller/dispatcher.rb

  第二步说了,如果没有相应的html存在的话,Rails将执行Dispatch过程。我们先来看一看handle_dispatch方法:

def handle_dispatch(req, res, origin = nil) #:nodoc:
  data = StringIO.new
  Dispatcher.dispatch(
    CGI.new("query", create_env_table(req, origin), StringIO.new(req.body || "")),
    ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS,
    data
  )
  ...
end


  这里,可以看到Dispatch的主角Dispatcher对象开始登场了。要执行dispatch,首先生成一个CGI对象(默认CGI类型是“query”,并且将环境配置传递给CGI对象,包括:主机名称,查询字符串,字符集,Path信息...等,以及默认的Session管理方式),其中的data表示对用户的返回数据(StringIO请参考相应的API)。然后执行Dispatcher的类方法dispatch。此方法内容如下:

class Dispatcher
  class << self
    # Backward-compatible class method takes CGI-specific args. Deprecated
    # in favor of Dispatcher.new(output, request, response).dispatch.
    def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
      new(output).dispatch_cgi(cgi, session_options)
    end
  ...
end


  此类方法将生成一个Dispatcher实例,并调用其dispatch_cgi实例方法(从前面的方法调用,我想不难看出每一个参数是什么)。我们继续接着看dispatch_cgi方法:

def dispatch_cgi(cgi, session_options)
  if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new }
    @request = CgiRequest.new(cgi, session_options)
    @response = CgiResponse.new(cgi)
    dispatch
  end
rescue Exception => exception
  failsafe_rescue exception
end


  前面也有request和reponse,这里又生成了一个request和response。我是这样理解的,前面handle_dispatch接收的req和res是“原生”的对象----WEBRick::HTTPRequest和WEBRick::HTTPResponse(是WEBRick和Rails的通讯方式),而这里的request和response是CgiRequest和CgiResponse对象,是针对Dispatch的通讯(CgiRequest和CgiResponse的细节这里先略过,我们看看主流程)。有了request和response对象,真正的dispatch过程开始了:

def dispatch
  run_callbacks :before
  handle_request
rescue Exception => exception
  failsafe_rescue exception
ensure


  先来看一看run_callbacks:

def run_callbacks(kind, enumerator = :each)
  callbacks[kind].send!(enumerator) do |callback|
    case callback
    when Proc; callback.call(self)
    when String, Symbol; send!(callback)
    when Array; callback[1].call(self)
    else raise ArgumentError, "Unrecognized callback #{callback.inspect}"
    end
  end
end


  其中的callbacks(hash)是Dispatcher的类属性,用来执行一些dispatch前,后,准备的工作。这里,我们直接看看他的值
  {:before=>[:reload_application,:prepare_application],:after=>[:flush_logger,:cleanup_application],:prepare=>[:activerecord_instantiate_observers,"Proc"]}。就这里而言,我们要执行所有:before的callback,不一一列出,只看其中一个:

def reload_application
  if Dependencies.load?
    Routing::Routes.reload
    self.unprepared = true
  end
end


  这段代码揭示了在程序运行时,我们改动了routes.rb中的路由信息后,下一次request马上就能生效的原理。另外prepare_application的功能是require我们熟悉的Controller/application.rb,并且验证ActiveRecord的数据库连接是否正常(当然,你需要使用AR框架的话)。好了,这里稍微偏离了主线,接下来,让我们回到dispatch方法中,看看下面的调用handle_request:

def handle_request
  @controller = Routing::Routes.recognize(@request)
  @controller.process(@request, @response).out(@output)
end


  上面的代码非常直观,首先通过Routing系统,根据客户端的request找到相应的controller,然后执行并且将返回数据写入到@output中(这也是我前面提到的那个StringIO对象)。至于如何具体找到controller的,进入下一步吧。

  (四)寻找controller
  源代码:/actionpack-2.0.2/lib/action_controller/routing.rb
  从前面的方法调用中,我们看出寻找controller的入口是RouteSet对象的recognize方法(还记得Routing::Routes是一个RouteSet的对象实例吗?要理解Rails中的Routing子系统,我在第二篇文章中整理的那张类图十分重要!)。下面看看此方法的具体内容:

def recognize(request)
  params = recognize_path(request.path, extract_request_environment(request))
  request.path_parameters = params.with_indifferent_access
  "#{params[:controller].camelize}Controller".constantize
end


  首先调用recognize_path方法,其中request.path是客户端请求的路径(比如:如果客户端访问地址是http://localhost:3000/posts ,那么此参数就是/posts,extract_request_environment(request)方法只是得到请求的http方法(get,post,put,delete),当然,此方法返回的结果params便是我们的Controller和Action。我们知道,Routing系统通过path和http verb就可以确定应该使用哪个Controller的哪个Action,下面看看他是怎么做到的吧:

 

def recognize_path(path, environment={})
  routes.each do |route|
    result = route.recognize(path, environment) and return result
  end

  allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }

  if environment[:method] && !HTTP_METHODS.include?(environment[:method])
    raise NotImplemented.new(*allows)
  elsif !allows.empty?
    raise MethodNotAllowed.new(*allows)
  else
    raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
  end
end


  如果你看过我的第二,三篇文章,我想你应该知道这里的routes数组就是Routing系统中庞大的路由表,routes数组的元素是Route对象,里面记录了相应的path pattern对应于哪个Controller的哪个Action方法。这里,通过path,和environment(http verb)参数,调用每一个Route对象的recognize方法,如果找到相应的Controller,则返回;如果未找到,则进行接下来的错误处理,这里我们可以看到很熟悉的“No route matches...”。那我们再来看看Route对象是如何通过Path和environment来识别Controller的,先来看看Route类的recognize方法:

def recognize(path, environment={})
  write_recognition
  recognize path, environment
end


  在recognize方法中调用recognize方法?传说中的死循环?呵呵。当然不是了,看完write_recognition你就知道是怎么回事了:

 

def write_recognition
  # Create an if structure to extract the params from a match if it occurs.
  body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
  body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"

  # Build the method declaration and compile it
  method_decl = "def recognize(path, env={})\n#{body}\nend"
  instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
  method_decl
end


  这里,Rails利用instance_eval重写了该对象(此Route对象)的recognize方法(可不是override哦)。完成重写后,将再次调用recognize方法,此时,这个方法已经是动态生成的了。那么现在我们来看看这个动态方法长什么样,这里,我假设客户端访问url为http://localhost:3000/posts ,并且,在routes.rb中,我们已经通过map.resources :posts建立了一系列针对posts的Route,其中当然包括“Get /posts/”(对应的Controller是posts,对应的Action是index)。那么在调用这个Route对象的write_recognition方法时,将会动态生成如下代码:

def recognize(path, env={})
  if (match = /\A\/posts\/?\Z/.match(path)) && conditions[:method] === env[:method]
    params = parameter_shell.dup 
    params
  end
end


  逻辑很简单,只是通过正则表达式来判断此Route的pattern是否与客户端请求的path一致,并且http verb也匹配,如果是的话,则将parameter_shell方法的结果dup出来,并且返回。
  (注意,这里if子句的条件是通过recognition_conditions方法根据不同Route的不同condition动态生成的。因此针对每个Route,此条件都不同。另外recognition_extraction方法我一直没搞懂他干什么用的-_-!)
  这里,我们还是看一看parameter_shell方法:

def parameter_shell
  @parameter_shell ||= returning({}) do |shell|
    requirements.each do |key, requirement|
      shell[key] = requirement unless requirement.is_a? Regexp
    end
  end
end


  无非就是将requirements(包括controller和action)塞到shell数组,然后返回。
  好啦,针对路由表中的每一个Route,调用其recognize方法,知道找到匹配的Route,然后将结果(controller和action数组)返回(如未找到匹配的,则进行错误处理),接下来,我们的思路得回到RouteSet对象的recognize方法,最终,使用#{params[:controller].camelize}Controller".constantize,将controller参数转换为首字符大写的形式,并且加上“Controller”字符串,最终将整个字符串(“PostsController”),转换为一个常量(PostsController,表示控制器对象),并且,调用此Controller的process方法(此方法其实是ActionController::Base的类方法),接下来的事,后续文章会继续分析。
  (这个过程也揭示了Rails中的“约定甚于配置”的精髓)
  (就目前而言,我们都在Rails的主线上游走,我觉得下篇文章,应该暂停一下,来看一些细节的东西,已达到更深入了解Rails的目的)

   发表时间:2008-03-26  
Mindjet MindManager
0 请登录后投票
   发表时间:2008-03-28  
您分析的很好!我想问您个问题:
关于rails的问题:就是我模型里面创建了一个回调的方法,功能是:比如:我要创建一个部落,用回调方法实现自动增加了创建者为酋长和管理员!但是我用夹具创建了一个部落,这些都没有显示出来!是怎么回事?是夹具在加载数据时饶过回调方法了吗?还是直接饶过模型,直接把数据加载到数据库里面?你有关于夹具的源代码吗?我想知道是怎么回事!
0 请登录后投票
   发表时间:2008-03-28  
你好,能否把相关源代码发给我?光看你的描述我也不清楚问题所在。短信联系
不知道下面的帖子是否对你有帮助
http://www.iteye.com/post/299671?page=3
0 请登录后投票
   发表时间:2008-06-28  
领导!
通过你的文章学习到很多东西!搞明白了RAILS的一个大概流程!谢谢
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics