代码之家  ›  专栏  ›  技术社区  ›  elasticsecurity

Ruby方法拦截

  •  5
  • elasticsecurity  · 技术社区  · 14 年前

    我想拦截Ruby类上的方法调用,并且能够在方法实际执行之前和之后做一些事情。我尝试了以下代码,但得到了错误:

    方法接收.rb:16:in before_filter': (eval):2:in 别名“方法”:未定义的方法 say_hello' for class 家庭作业 (名称错误) 发件人(eval):2:in“before_filter”

    有人能帮我做对吗?

    class MethodInterception
    
      def self.before_filter(method)
        puts "before filter called"
        method = method.to_s
        eval_string = "
          alias_method :old_#{method}, :#{method}
    
          def #{method}(*args)
            puts 'going to call former method'
            old_#{method}(*args)
            puts 'former method called'
          end
        "
        puts "going to call #{eval_string}"
        eval(eval_string)
        puts "return"
      end
    end
    
    class HomeWork < MethodInterception
      before_filter(:say_hello)
    
      def say_hello
        puts "say hello"
      end
    
    end
    
    3 回复  |  直到 14 年前
        1
  •  2
  •   Shinya    14 年前

    从原始代码更改的代码更少。我只修改了两行。

    class MethodInterception
    
      def self.before_filter(method)
        puts "before filter called"
        method = method.to_s
        eval_string = "
          alias_method :old_#{method}, :#{method}
    
          def #{method}(*args)
            puts 'going to call former method'
            old_#{method}(*args)
            puts 'former method called'
          end
        "
        puts "going to call #{eval_string}"
        class_eval(eval_string) # <= modified
        puts "return"
      end
    end
    
    class HomeWork < MethodInterception
    
      def say_hello
        puts "say hello"
      end
    
      before_filter(:say_hello) # <= change the called order
    end
    

    这很管用。

    HomeWork.new.say_hello
    #=> going to call former method
    #=> say hello
    #=> former method called
    
        2
  •  14
  •   Jörg W Mittag    14 年前

    我刚想到:

    module MethodInterception
      def method_added(meth)
        return unless (@intercepted_methods ||= []).include?(meth) && !@recursing
    
        @recursing = true # protect against infinite recursion
    
        old_meth = instance_method(meth)
        define_method(meth) do |*args, &block|
          puts 'before'
          old_meth.bind(self).call(*args, &block)
          puts 'after'
        end
    
        @recursing = nil
      end
    
      def before_filter(meth)
        (@intercepted_methods ||= []) << meth
      end
    end
    

    这样使用:

    class HomeWork
      extend MethodInterception
    
      before_filter(:say_hello)
    
      def say_hello
        puts "say hello"
      end
    end
    

    作品:

    HomeWork.new.say_hello
    # before
    # say hello
    # after
    

    代码中的基本问题是在 before_filter 方法,但在客户端代码中, 过滤器前 在实际定义方法之前,会尝试重命名不存在的方法。

    解决方案很简单:不要这样做?!

    好吧,好吧,也许不那么简单。你 能够 只需强迫客户随时打电话 过滤器前 之后 他们已经定义了他们的方法。然而,这是糟糕的API设计。

    因此,您必须以某种方式安排代码将方法的包装延迟到它实际存在为止。这就是我所做的:而不是重新定义 过滤器前 方法,我只记录了稍后要重新定义它的事实。然后,我做 实际的 重新定义 method_added 钩子。

    这有一个小问题,因为如果在 添加方法 当然,它会立即再次被调用并再次添加该方法,这将导致它再次被调用,依此类推。所以,我需要防止递归。

    注意这个解决方案实际上 在客户端上强制排序:而操作的版本 只有 如果你打电话就可以了 过滤器前 之后 定义方法时,只有当您调用它时,我的版本才起作用 之前 . 然而,它非常容易扩展,这样它就不会受到那个问题的影响。

    还要注意,我做了一些与问题无关的额外更改,但我认为这些更改更为鲁莽:

    • 使用mixin而不是类:继承是Ruby中非常有价值的资源,因为您只能从一个类继承。不过,混音器很便宜:你可以随心所欲地混音。另外:你真的能说家庭作业是一种系统的感觉吗?
    • 使用 Module#define_method 而不是 eval : 评价 是邪恶的。”纳夫说。(完全没有理由使用 评价 首先,在操作代码中。)
    • 使用方法包装技术而不是 alias_method : 别名\方法 链技术使用无用的 old_foo old_bar 方法。我喜欢干净的名称空间。

    我刚刚修复了我上面提到的一些限制,并添加了更多的功能,但是我太懒于重写我的解释,所以我将修改后的版本重新发布到这里:

    module MethodInterception
      def before_filter(*meths)
        return @wrap_next_method = true if meths.empty?
        meths.delete_if {|meth| wrap(meth) if method_defined?(meth) }
        @intercepted_methods += meths
      end
    
      private
    
      def wrap(meth)
        old_meth = instance_method(meth)
        define_method(meth) do |*args, &block|
          puts 'before'
          old_meth.bind(self).(*args, &block)
          puts 'after'
        end
      end
    
      def method_added(meth)
        return super unless @intercepted_methods.include?(meth) || @wrap_next_method
        return super if @recursing == meth
    
        @recursing = meth # protect against infinite recursion
        wrap(meth)
        @recursing = nil
        @wrap_next_method = false
    
        super
      end
    
      def self.extended(klass)
        klass.instance_variable_set(:@intercepted_methods, [])
        klass.instance_variable_set(:@recursing, false)
        klass.instance_variable_set(:@wrap_next_method, false)
      end
    end
    
    class HomeWork
      extend MethodInterception
    
      def say_hello
        puts 'say hello'
      end
    
      before_filter(:say_hello, :say_goodbye)
    
      def say_goodbye
        puts 'say goodbye'
      end
    
      before_filter
      def say_ahh
        puts 'ahh'
      end
    end
    
    (h = HomeWork.new).say_hello
    h.say_goodbye
    h.say_ahh
    
        3
  •  0
  •   Swanand    14 年前

    JRG W Mittag的解决方案相当不错。如果您想要更健壮的(经过良好测试的)东西,最好的资源是Rails回调模块。