developmentを実現したいのでコードを読んでみる#1

Railsのdevelopmentモードのように特定のディレクトリ以下のソースコードを適切なタイミングで読み直す機能を作りたいのですが、実際Railsって何やっているのか分からんので調べます。

cache_classes

railsアプリの config/environments/development.rb には大抵

  config.cache_classes = false

と書いてあります。クラスのキャッシュを無効にするって意味っすね。

この設定がどこで使われているのか、railsの設定はrailtiesに書いてあるのでgrepしてみました。

-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/railties-3.1.3/lib/rails/" -*-
Grep started at Sun Jan  8 20:35:42

grep -nri cache_classes .
./application/bootstrap.rb:64:        ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load
./application/configuration.rb:9:                    :cache_classes, :cache_store, :consider_all_requests_local,
./application/configuration.rb:95:        self.cache_classes = true
./application/finisher.rb:49:        if config.cache_classes && !$rails_rake_task
./application/finisher.rb:70:        if config.cache_classes && !config.dependency_loading
./application.rb:22:  # "allow_concurrency", "cache_classes", "consider_all_requests_local", "filter_parameters",
./application.rb:168:        middleware.use ::ActionDispatch::Reloader unless config.cache_classes
./generators/rails/app/templates/config/environments/development.rb.tt:7:  config.cache_classes = false
./generators/rails/app/templates/config/environments/production.rb.tt:5:  config.cache_classes = true
./generators/rails/app/templates/config/environments/test.rb.tt:8:  config.cache_classes = true
./railtie/configuration.rb:39:      # Third configurable block to run. Does not run if config.cache_classes
実はloadが使われる

developmentモードではActiveSupport::Dependenciesはrequireではなく、loadを使ってロードします。
ActiveSupport::Dependenciesが動く場合ってことは、const_missingあたりから命名規則にしたがってファイルをロードするあたりの話っすね、たぶん。

./application/bootstrap.rb:64:        ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load
middleware.use ::ActionDispatch::Reloader
./application.rb:168:        middleware.use ::ActionDispatch::Reloader unless config.cache_classes

一番大事そうなのはココ。developmentモードではミドルウェアがActionDispatch::Reloaderを使うそうです。
middlewareってRackとかの話だよね?これまでちゃんと調べたことなかったので、middlewareを調べましょう!

middleware

まず、ここに登場しているmiddlewareは何かと言えば、ここに書いてある。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/application.rb#L146

    def default_middleware_stack
      ActionDispatch::MiddlewareStack.new.tap do |middleware|
        if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache
          require "action_dispatch/http/rack_cache"
          middleware.use ::Rack::Cache, rack_cache
        end
        #...
      end
   end

ActionDispatch::MiddlewareStack.new.tapに渡されるブロックの引数でした。

ActionDispatch::MiddlewareStack

これは何ぞ?と検索してみる。ActionDispatchだからactionpack以下にあるはず・・・
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb
ココですね。

ActionDispatch::MiddlewareStackクラスの定義の中に、Middlewareクラスの定義があって、ActionDispatch::MiddlewareStackクラスの具体的な記述は
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L53
以降に書かれています。

include Enumerableとかしてるし、initializeメソッドで

@middlewares = []

とかやっているので、前述のMiddlewareクラスのオブジェクトを複数個持ってなんかする奴なんでしょうな。

具体的な使われ方として、

middleware.use ::ActionDispatch::Reloader

が意味するところをまずは知りたいんだけど、useメソッドの定義を読むと引数とブロックを、前述のMiddleware.newの引数に渡しちゃってmiddlewareを生成してそれをmiddlewaresに追加してるってことっすね。

    def use(*args, &block)
      middleware = self.class::Middleware.new(*args, &block)
      middlewares.push(middleware)
    end

https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L104

ActionDispatch::MiddlewareStack::Middleware

じゃあその前述のMiddlewareを知っておきたいところなんだけど、
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L6

      def initialize(klass_or_name, *args, &block)
        @klass = nil

        if klass_or_name.respond_to?(:name)
          @klass = klass_or_name
          @name  = @klass.name
        else
          @name  = klass_or_name.to_s
        end

        @classcache = ActiveSupport::Dependencies::Reference
        @args, @block = args, block
      end

引数の最初にClassかクラスの名前を期待していて、Classが指定された場合は@klassに代入されるけど、そうじゃない場合は@klassはnilのまま。
それ以外はそのまま@argsに代入される。こいつはどこで使われるかって言うと、

      def build(app)
        klass.new(app, *args, &block)
      end

buildメソッドで指定されたklass.newの引数として、このメソッドの引数appとともに@argsの内容が渡されるんだけど、klassメソッドにはこう書いてある。

      def klass
        @klass || classcache[@name]
      end

名前を指定した場合には、classcacheから検索するようになっています。
classcacheはinitializeで指定されているActiveSupport::Dependencies::Referenceですね。

middleware.use ::ActionDispatch::Reloader

もう一度考えてみると、ActionDispatch::MiddlewareStackのインスタンスであるmiddlewareに::ActionDispatch::Reloaderをクラスを指定してuseさせているので、

ActionDispatch::MiddlewareStack::Middleware.new(::ActionDispatch::Reloader)

で生成されたものがmiddlewareには記憶されている。

あとは、どこかでこれのbuildメソッドが呼び出されるタイミングがあるはずなんだけど、
実は ActionDispatch::MiddlewareStack#build で ActionDispatch::MiddlewareStack::Middleware#build が呼び出されます。
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L109

    def build(app = nil, &block)
      app ||= block
      raise "MiddlewareStack#build requires an app" unless app
      middlewares.reverse.inject(app) { |a, e| e.build(a) }
    end

こいつはどこから呼び出されるのか?これはRails::Engine#appから呼び出されます。

Rails::Engine

railtiesをActionDispatch::MiddlewareStackでgrepしてみると、以下の2つが見つかります。

-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/railties-3.1.3/lib/" -*-
Grep started at Sun Jan  8 22:52:59

grep -nri ActionDispatch::MiddlewareStack .
./rails/application.rb:146:      ActionDispatch::MiddlewareStack.new.tap do |middleware|
./rails/engine.rb:606:      ActionDispatch::MiddlewareStack.new

それぞれ Rails::Appliation#default_middleware_stack と Rails::Engine#default_middleware_stack から呼び出されています。

Rails::Appliation? そうです。railsアプリのconfig/application.erbに記述されるアレです。

module Blog
  class Application < Rails::Application

https://github.com/rails/rails/blob/master/railties/guides/code/getting_started/config/application.rb#L12


で、この Rails::Application は Rails::Engine を継承しているわけですね。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/application.rb#L36

じゃあこの Rails::Engine はというと、 Rails::Railtie を継承しています。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/engine.rb#L333

Rails::Railtie は何も継承していません。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/railtie.rb#L113

まとめるとこういう継承をしているわけですね。

Rails::Railtie <|---- Rails::Engine <|---- Rails::Application

Rails::Applicationのインスタンスがいつ生成されるのかが知りたくなるわけですが、これを追っかけるのは大変!と思っていたら強い味方発見。
http://guides.rubyonrails.org/initialization.html
すばらしい!


続きはまた明日。