Railtieのサブクラスで呼び出したinitializerブロックが読み込まれる仕組みをコードを読んで紐解いてみました。Railsは5.1.2です
Railtieでのinitializer呼び出し
initializerメソッドはRails::Initializableモジュールに定義されています。Rails::Initializable.initializerメソッドではInitializerのインスタンスを作ってCollectionに追加します。opts[:after]の行はTSortによるソート用に定義しており、基本的にはinitializerが呼び出された順にブロックの処理が走るようになっています。require "tsort"
module Rails
module Initializable
module ClassMethods
...
def initializers
@initializers ||= Collection.new
end
...
def initializer(name, opts = {}, &blk)
raise ArgumentError, "A block must be passed when defining an initializer" unless blk
opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] }
initializers << Initializer.new(name, nil, opts, &blk)
end
...
これらはクラスメソッドなので各Railtieのクラスインスタンス変数に@initializersとしてInitializerのCollectionが格納されることになります。
これらの呼び出しはRails::Applicationから行います。
config/environment.rbで Rails.application.initialize!
を呼びだします
module Rails
class Application < Engine
...
# Initialize the application passing the given group. By default, the
# group is :default
def initialize!(group = :default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
self
end
...
Rails::Initializable#run_initializersは自身のinitializersをTSortでトポロジカルソートしたものを順にrunしていきます。
module Rails
module Initializable
...
def run_initializers(group = :default, *args)
return if instance_variable_defined?(:@ran)
initializers.tsort_each do |initializer|
initializer.run(*args) if initializer.belongs_to?(group)
end
@ran = true
end
...
Initializer#runはRails::Initializable.initializerで定義したブロックを実行するメソッドです。
module Rails
module Initializable
class Initializer
...
def run(*args)
@context.instance_exec(*args, &block)
end
...
Rails::Initializableに#initializersが定義されているのですが、Rails::Applicationにも#initializersが定義されているので、こちらが呼び出されます。
def initializers #:nodoc:
Bootstrap.initializers_for(self) +
railties_initializers(super) +
Finisher.initializers_for(self)
end
railties_initializersはordered_railtiesのinitializersの集合になります。
def railties_initializers(current) #:nodoc:
initializers = []
ordered_railties.reverse.flatten.each do |r|
if r == self
initializers += current
else
initializers += r.initializers
end
end
initializers
end
ordered_railtiesにはrailtiesの中身が入り、railtiesはRailtiesのインスタンスです。Rails::Engine::Railties#eachメソッドで::Rails::Railtieのサブクラス、::Rails::Engineのサブクラスのインスタンスがセットされます。これによってRailtieを継承したクラスのinitializerで渡したブロックが順に実行されることになります。
module Rails
class Engine < Railtie
class Railties
include Enumerable
attr_reader :_all
def initialize
@_all ||= ::Rails::Railtie.subclasses.map(&:instance) +
::Rails::Engine.subclasses.map(&:instance)
end
def each(*args, &block)
_all.each(*args, &block)
end
def -(others)
_all - others
end
end
end
end
TSortによるトポロジカルソート
さて、Railtieでは以下のようにbefore, afterキーワード引数によってinitializerの実行順の制御をすることができました。module HttpActionMailer
class Railtie < Rails::Railtie
initializer 'http_action_mailer.add_delivery_method', before: 'action_mailer.set_configs' do
ActiveSupport.on_load :action_mailer do
ActionMailer::Base.add_delivery_method(:http, HttpActionMailer::DeliveryMethod)
end
...
これらのInitializerの順番を制御するためにRubyのTSortというライブラリを使っています。TSortはトポロジカルソートができるライブラリで、各ノードの関係性を使ったソートができます。
TSortの簡単な利用例は公式のリファレンスに書いてあるとおり、こんな感じです↓
require 'tsort'
class Hash
include TSort
alias tsort_each_node each_key
def tsort_each_child(node, &block)
fetch(node).each(&block)
end
end
{1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort
# [3, 2, 1, 4]
例だと
- 1は2、3より後
- 2は3より後
- 3の後は特に指定なし
- 4の後は特に指定なし
initializerでは以下のようなCollectionクラスを設定してinitializerメソッドで定義されたInitializerクラスを良い感じにソートしています。
class Collection < Array
include TSort
alias :tsort_each_node :each
def tsort_each_child(initializer, &block)
select { |i| i.before == initializer.name || i.name == initializer.after }.each(&block)
end
def +(other)
Collection.new(to_a + other.to_a)
end
end
自分のブロックの前に実行すべきノードをselectで集めることで、before/afterの実行順序制御を実現しています。