天天看點

Ruby On Rails-2.0.2源代碼分析(4)-尋找Controller前言尋找Controller

  • 前言

  經過一番試驗和考慮...一,我嘗試了一些思維導圖工具(MindMapper,FREEMIND),但我始終沒有找到一種好的方式将自己學習Rails源代碼的思路表述出來,就此作罷(順便問問,有研究思維導圖的同學麼?能否推薦兩個自己覺得用起來比較順手的工具)。二,不再打算整理代碼運作順序圖,對不熟悉Rails源代碼的同學們來說,這個圖可能的确沒什麼幫助,甚至會把人搞暈。我現在打算從Rails源代碼功能點的角度出發,根據具體功能點,結合Rails源代碼進行學習,整理,總結。如果某些源代碼比較複雜,牽涉類比較繁多,我仍然打算整理一個類圖,從一個高的層次了解系統内部對象的關系。

  前面三篇文章,我們看到了Rails啟動的大緻功能和流程,包括初始化多種環境變量,初始化Route表,啟動Web伺服器開始偵聽用戶端請求。。。那麼接下來,當然是開門迎客,等待用戶端(浏覽器)的請求,并進行處理,最終将結果傳回用戶端(浏覽器)呈現。那麼熟悉Rails的同學都知道,首先,Rails必須根據用戶端的一個請求,決定将要執行哪個Controller的哪個Action,這也是本文的主要目的。

  • 尋找Controller

首先,我們先來看一看Rails通過用戶端請求,查找Controller的大緻流程圖

Ruby On Rails-2.0.2源代碼分析(4)-尋找Controller前言尋找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的目的)