`
missall
  • 浏览: 127632 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

rails2.0.2源码分析-寻找controller

阅读更多
  • 前言

  经过一番试验和考虑...一,我尝试了一些思维导图工具(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的目的)

 

分享到:
评论
2 楼 lcyi.sq.cn@gmail.com 2008-06-17  
谢谢 ,这一点看不明白
1 楼 lcyi.sq.cn@gmail.com 2008-06-17  
最终将整个字符串(“PostsController”),转换为一个常量(PostsController,表示控制器对象)  


这个是什么意思呢?怎么将一个字符串转化为一个控制器对象的?我看了constantize这个方法不知道怎么回事

相关推荐

    rails 2.0.2 分页 需另外下载插件

    在Ruby on Rails框架中,`Rails 2.0.2`是一个较早的版本,而分页功能在那个时期并不像现在的Rails应用那样内置在框架内。为了实现分页,开发者通常需要安装并使用第三方插件,比如"will_paginate"。这个插件允许你在...

    ruby1.8.6 + rails2.0.2 安装配置 详细说明

    在本文中,我们将深入探讨如何在您的计算机上安装和配置Ruby 1.8.6、Rails 2.0.2、RadRails 0.7.2 IDE以及MySQL数据库。这是一个适用于初学者和有一定经验的开发者的技术指南,旨在帮助您创建一个稳定的开发环境,...

    Ruby on Rails安装指南(Ruby 1.8.6+Rails 2.0.2)

    在命令行工具中执行以下命令来安装Rails 2.0.2: ```bash gem install rails -v 2.0.2 ``` 该命令会从Ruby的包管理库中查找并安装指定版本的Rails。安装完成后,同样通过`rails -v`来检查是否安装成功。 **知识点...

    rails-chm-2-0-2.rar

    `rails-documentation-2-0-2.chm` 文件详细涵盖了这些概念,包含了关于Rails 2.0.2的API参考、教程和指南。通过仔细阅读和实践,开发者能够深入理解Rails的工作原理,并有效地开发出高效、可维护的Web应用。

    rails-hackernews-reddit-producthunt-clone, 黑客 news/reddit/social 链接分享网站 用 Rails 构建.zip

    rails-hackernews-reddit-producthunt-clone, 黑客 news/reddit/social 链接分享网站 用 Rails 构建 Rails 上的 Reddit-Hackernews-ProductHunt克隆演示 这是一个 readme.md的Ruby on Rails 应用程序,模仿了 Hacker...

    rails-exporter-源码.rar

    《Rails Exporter 源码解析》 Rails Exporter 是一个用于 Rails 应用程序的开源工具,主要用于数据导出功能。源码分析将帮助我们深入理解其内部工作原理,以便更好地利用它来优化我们的应用。 一、Rails 框架基础 ...

    Ruby on Rails 初体验--北大青鸟教师专题讲座PPT

    RoR遵循MVC(Model-View-Controller)架构模式,旨在简化Web应用开发,降低开发者的工作负担,提高开发效率。该框架的核心设计理念包括“Don’t Repeat Yourself”(DRY)和“Convention Over Configuration”(CoC...

    rails-controller-testing:将`assigns`和`assert_template`带回到您的Rails测试中

    gem 'rails-controller-testing' 然后执行: $ bundle 或将其自己安装为: $ gem install rails-controller-testing 规范 参见 。 从3.5.0版开始,rspec-rails会自动与该gem集成。 将gem添加到您的Gemfile就足够...

    rails3-mongoid-devise, 示例 Rails 3.2应用,带有数据 Mongoid,用于验证.zip

    rails3-mongoid-devise, 示例 Rails 3.2应用,带有数据 Mongoid,用于验证 Rails 4.1有关设计的Rails 4.1示例应用程序,请参见:rails设计有一个用于设计的教程:Rails 设计教程。类似示例和教程这是来自 RailsApps...

    Agile Web Development with Rails-Second Edition-Beta一书例子

    《Agile Web Development with Rails-Second Edition-Beta》是一本专注于使用Ruby on Rails框架进行敏捷Web开发的书籍。这本书的实例部分提供了丰富的代码示例,帮助读者深入理解Rails的架构和开发流程。在本压缩包...

    rails-react-components-源码.rar

    本文将深入探讨"rails-react-components-源码.rar"中的关键知识点,帮助开发者理解如何在Rails应用中集成React组件。 1. **React组件化开发** React的核心概念是组件,它允许我们将UI拆分为独立、可重用的部分。在...

    awesome-rails-gem-zh_CN, Rails 常用 Gem 列表 - Awesome Rails Gem 中文版.zip

    在这个压缩包中,`awesome-rails-gem-zh_CN-master`可能是项目源码或文档的主目录。以下是一些可能包含在列表中的关键Rails Gem及其功能简介: 1. **Devise**:这是一个灵活的身份认证解决方案,提供了一套完整的...

    rails-documentation-1-2-1.zip

    标题 "rails-documentation-1-2-1.zip" 暗示这是一份关于 Ruby on Rails 框架的文档,版本为 1.2.1。Ruby 是一种面向对象的编程语言,而 Rails 是一个基于 Ruby 的开源 Web 应用程序框架,遵循 Model-View-...

    rails-yelp-mvp-源码.rar

    【标题】"rails-yelp-mvp-源码" 指的是一个基于Rails框架开发的类似于Yelp(美国知名餐饮评论网站)的最小可行产品(Minimum Viable Product, MVP)的源代码。Rails是Ruby编程语言的一个流行Web开发框架,以其“约定...

    rails-documentation-2-0-2

    rails-documentation-2-0-2

    Rails进行敏捷Web开发(所有版本的源码rails3.0-4.0)

    Rails是Ruby语言的一个著名Web应用框架,以敏捷开发和“约定优于配置”...通过对这些版本的源码进行分析和学习,开发者不仅可以提升对Rails框架的理解,还能在实际项目中运用这些知识,编写出更高效、更安全的Web应用。

    Rails管理框架upmin-admin.zip

    upmin-admin 是一个为 Rails 应用开发的开源管理框架。用来管理 Rails 应用中各种对象(如 Model、View 和 Controller )。 标签:upmin

    rails-builds-test-源码.rar

    在"rails-builds-test-源码.rar"这个压缩包中,我们很显然会接触到一个使用Rails框架构建的测试项目。接下来,我们将深入探讨Rails的几个关键知识点,以及如何通过源码来理解其工作原理。 1. **Gemfile与Gemfile....

Global site tag (gtag.js) - Google Analytics