rails 中的 callback 使用 ActiveSupport::Callbacks 模块来实现,Api 文档里有一个相应的例子
使用的方法是先创建一个空的 callback 链表,给这个 callback 链表 push 一些方法,然后在需要运行 callback 的地方调用该队列
class Record
include ActiveSupport::Callbacks
define_callbacks :save
def save
run_callbacks :save do
puts "- save"
end
end
end
class PersonRecord < Record
set_callback :save, :before, :saving_message
def saving_message
puts "saving..."
end
set_callback :save, :after do |object|
puts "saved"
end
end
person = PersonRecord.new
person.save
Output:
saving... -save saved
define_callbacks 方法会定义一个类变量,然后把一个空的 callback 链赋值给这个类变量。这个变量的值可以被子类继承,一旦该类被继承,子类也会拥有这个 callback 链
同时在这个方法里会根据 name 来创建一个 run 方法以供调用
def define_callbacks(*names)
options = names.extract_options!
names.each do |name|
class_attribute "_#{name}_callbacks", instance_writer: false
set_callbacks name, CallbackChain.new(name, options)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
__run_callbacks__(_#{name}_callbacks, &block)
end
RUBY
end
end
def set_callbacks(name, callbacks) # :nodoc:
send "_#{name}_callbacks=", callbacks
end
set_callback 方法会将一个 hook_method 注入 Callback 链
def set_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(filter_list, block)
self_chain = get_callbacks name
mapped = filters.map do |filter|
Callback.build(self_chain, filter, type, options)
end
__update_callbacks(name) do |target, chain|
options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
target.set_callbacks name, chain
end
end
run_callbacks 方法会调用前面 define_callbacks 方法里定义的 run 方法
def run_callbacks(kind, &block)
send "_run_#{kind}_callbacks", &block
end
run 方法内部根据 callback 链生成一个 callback 队列,然后直接运行这个 callback 队列
def __run_callbacks__(callbacks, &block)
if callbacks.empty?
yield if block_given?
else
runner = callbacks.compile
e = Filters::Environment.new(self, false, nil, block)
runner.call(e).value
end
end
contoller 中的 callback 链在 AbstractController::Callbacks 中定义
module AbstractController
extend ActiveSupport::Autoload
...
autoload :Callbacks
...
end
module AbstractController
module Callbacks
...
included do
define_callbacks :process_action,
terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.performed? },
skip_after_callbacks_if_terminated: true
end
...
end
end
在 controller 里我们不使用 set_callback 来定义 callback 函数,因为 rails wrapper 了一层 help 方法,可以省略一些参数
[:before, :after, :around].each do |callback|
define_method "#{callback}_action" do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
set_callback(:process_action, callback, name, options)
end
end
define_method "#{callback}_filter" do |*names, &blk|
ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will be removed in Rails 5.1. Use #{callback}_action instead.")
send("#{callback}_action", *names, &blk)
end
define_method "prepend_#{callback}_action" do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
set_callback(:process_action, callback, name, options.merge(:prepend => true))
end
end
define_method "prepend_#{callback}_filter" do |*names, &blk|
ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use prepend_#{callback}_action instead.")
send("prepend_#{callback}_action", *names, &blk)
end
# Skip a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
define_method "skip_#{callback}_action" do |*names|
_insert_callbacks(names) do |name, options|
skip_callback(:process_action, callback, name, options)
end
end
define_method "skip_#{callback}_filter" do |*names, &blk|
ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use skip_#{callback}_action instead.")
send("skip_#{callback}_action", *names, &blk)
end
# *_action is the same as append_*_action
alias_method :"append_#{callback}_action", :"#{callback}_action"
define_method "append_#{callback}_filter" do |*names, &blk|
ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use append_#{callback}_action instead.")
send("append_#{callback}_action", *names, &blk)
end
end
根据代码,在 rails5.1 里调用各种 xxxxx_filter 的结果就是直接转发给 xxxxx_action,同时给 xxxxx_action 取了个别名叫 append_xxxxx_action
既然知道了 callbacks 链的 name 是:process_action,我们就可以用 define_callbacks 生成的方法打印出 callbacks 链
class Api::Test < ApplicationController
puts _process_action_callbacks
before_action :authenticate_api_user!, only: :create
puts _process_action_callbacks
...
end
before_action 前,callbacks 链里只有两个 callback
#<ActiveSupport::Callbacks::CallbackChain:0x0055aba4e7c278
@callbacks=nil,
@chain=
[#<ActiveSupport::Callbacks::Callback:0x0055aba4e7c980
@chain_config=
{:scope=>[:kind],
:terminator=>#<Proc:0x0055aba4e2fd88@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/callbacks.rb:12 (lambda)>,
:skip_after_callbacks_if_terminated=>true},
@filter=:set_request_start,
@if=[],
@key=:set_request_start,
@kind=:before,
@name=:process_action,
@unless=[]>,
#<ActiveSupport::Callbacks::Callback:0x0055aba4e7c3b8
@chain_config=
{:scope=>[:kind],
:terminator=>#<Proc:0x0055aba4e2fd88@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/callbacks.rb:12 (lambda)>,
:skip_after_callbacks_if_terminated=>true},
@filter=:update_auth_header,
@if=[],
@key=:update_auth_header,
@kind=:after,
@name=:process_action,
@unless=[]>],
@config=
{:scope=>[:kind],
:terminator=>#<Proc:0x0055aba4e2fd88@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/callbacks.rb:12 (lambda)>,
:skip_after_callbacks_if_terminated=>true},
@mutex=#<Thread::Mutex:0x0055aba4e7c228>,
@name=:process_action>
执行了 before_action 后,callback 链里增加了一个 callback
#<ActiveSupport::Callbacks::CallbackChain:0x0055aba4bcd9f0
@callbacks=nil,
@chain=
...同上省略
#<ActiveSupport::Callbacks::Callback:0x0055aba4c14968
@chain_config=
{:scope=>[:kind],
:terminator=>#<Proc:0x0055aba4e2fd88@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/callbacks.rb:12 (lambda)>,
:skip_after_callbacks_if_terminated=>true},
@filter=:authenticate_api_user!,
@if=[#<Proc:0x0055aba4c4c340@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/callbacks.rb:52>],
@key=:authenticate_api_user!,
@kind=:before,
@name=:process_action,
@unless=[]>],
@config=
{:scope=>[:kind],
:terminator=>#<Proc:0x0055aba4e2fd88@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/callbacks.rb:12 (lambda)>,
:skip_after_callbacks_if_terminated=>true},
@mutex=#<Thread::Mutex:0x0055aba4bb1ea8>,
@name=:process_action>
试着打印生成的 callback 队列
_process_action_callbacks.compile
=> #<ActiveSupport::Callbacks::CallbackSequence:0x0055aba3454738
@after=[#<Proc:0x0055aba34541c0@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:216>],
@before=
[#<Proc:0x0055aba3454080@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:163>,
#<Proc:0x0055aba34543a0@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:144>],
@call=#<Proc:0x0055aba3454698@/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:506>>
# Override AbstractController::Base's process_action to run the
# process_action callbacks around the normal behavior.
def process_action(*args)
run_callbacks(:process_action) do
super
end
end
前面的 set 和 define 都发生在类源文件被 load 的过程中,但是 run_callbacks 在何时被调用却并不清晰,因此在 controller 中直接打印调用栈来查看
[
...
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:101:in `__run_callbacks__'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:750:in `_run_process_action_callbacks'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:90:in `run_callbacks'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/callbacks.rb:19:in `process_action'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_controller/metal/rescue.rb:20:in `process_action'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_controller/metal/instrumentation.rb:32:in `block in process_action'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/notifications.rb:164:in `block in instrument'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/notifications/instrumenter.rb:21:in `instrument'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/notifications.rb:164:in `instrument'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_controller/metal/instrumentation.rb:30:in `process_action'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_controller/metal/params_wrapper.rb:248:in `process_action'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activerecord-5.0.1/lib/active_record/railties/controller_runtime.rb:18:in `process_action'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/abstract_controller/base.rb:126:in `process'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_controller/metal.rb:190:in `dispatch'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_controller/metal.rb:262:in `dispatch'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/routing/route_set.rb:50:in `dispatch'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/routing/route_set.rb:32:in `serve'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/journey/router.rb:39:in `block in serve'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/journey/router.rb:26:in `each'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/journey/router.rb:26:in `serve'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/routing/route_set.rb:725:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/warden-1.2.7/lib/warden/manager.rb:36:in `block in call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/warden-1.2.7/lib/warden/manager.rb:35:in `catch'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/warden-1.2.7/lib/warden/manager.rb:35:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/etag.rb:25:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/conditional_get.rb:25:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/head.rb:12:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activerecord-5.0.1/lib/active_record/migration.rb:553:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/callbacks.rb:38:in `block in call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:97:in `__run_callbacks__'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:750:in `_run_call_callbacks'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/callbacks.rb:90:in `run_callbacks'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/callbacks.rb:36:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/executor.rb:12:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/remote_ip.rb:79:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/debug_exceptions.rb:49:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/show_exceptions.rb:31:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/railties-5.0.1/lib/rails/rack/logger.rb:36:in `call_app'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/railties-5.0.1/lib/rails/rack/logger.rb:24:in `block in call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/tagged_logging.rb:69:in `block in tagged'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/tagged_logging.rb:26:in `tagged'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/tagged_logging.rb:69:in `tagged'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/railties-5.0.1/lib/rails/rack/logger.rb:24:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/request_id.rb:24:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/runtime.rb:22:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/executor.rb:12:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/actionpack-5.0.1/lib/action_dispatch/middleware/static.rb:136:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/sendfile.rb:111:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/rack-cors-0.4.1/lib/rack/cors.rb:81:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/railties-5.0.1/lib/rails/engine.rb:522:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/configuration.rb:226:in `call'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/server.rb:578:in `handle_request'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/server.rb:415:in `process_client'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/server.rb:275:in `block in run'",
"/home/adam/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/puma-3.7.0/lib/puma/thread_pool.rb:120:in `block in spawn_thread'"]
大致上一次请求从 puma 侦听到 IO 并交给 rack,经过路由找到对应的 controller 之后,就会进到 process_action
很多 module 都有这个方法,利用 super 根据 include 进来的顺序倒顺执行,控制响应请求的过程,callback 是其中一环