Rails 详解 Rails Controller 中的 Callback

adamshen · 2017年02月22日 · 5267 次阅读
本帖已被管理员设置为精华贴

一、ActiveSupport::Callbacks

1.1 例子

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

1.2 初始化 callback 链

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

1.3 注册 hook_method

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

1.4 调用 callback 链

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

二、Controller 中的 callback

2.1 定义 callback 链

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

2.2 注册 hook_method

在 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>>

2.3 调用 callback 链

# 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 是其中一环

huacnlee 将本帖设为了精华贴。 02月22日 17:10
需要 登录 后方可回复, 如果你还没有账号请 注册新账号