Ruby方法拦截

我想拦截ruby-class上的方法调用,并且能够在实际执行方法之前和之后执行某些操作。 我尝试了以下代码,但得到错误:

MethodInterception.rb:16:in before_filter': (eval):2:in alias_method’:undefined method say_hello' for class HomeWork’(NameError)from(eval):2:in`after_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 

更少的代码从原来改变了。 我只修改了2行。

 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 

我想出了这个:

 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方法中重命名了该方法,但在客户端代码中,在实际定义方法之前调用了before_filter ,从而导致尝试重命名不存在的方法。

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

好吧,好吧,也许不那么简单。 您可以简单地强制客户定义方法始终调用before_filter 。 但是,这是糟糕的API设计。

因此,您必须以某种方式安排您的代码推迟方法的包装,直到它实际存在。 这就是我所做的:不是在before_filter方法中重新定义方法,而是仅记录以后要重新定义的事实。 然后,我在method_added钩子中进行实际的重新定义。

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

请注意,此解决方案实际上强制在客户端上进行排序:虽然OP的版本 定义方法调用before_filter时才有效,但我的版本仅在您之前调用它时才有效 。 但是,它很容易扩展,因此它不会遇到这个问题。

另请注意,我做了一些与问题无关的其他更改,但我认为更多的是Rubyish:

  • 使用mixin而不是类:inheritance是Ruby中非常有价值的资源,因为你只能从一个类inheritance。 然而,Mixins很便宜:你可以根据需要混合使用。 另外:你真的可以说Homework IS-A MethodInterception吗?
  • 使用Module#define_method而不是evaleval是邪恶的。 '努夫说。 (在OP的代码中,首先使用eval是完全没有理由的。)
  • 使用方法换行技术而不是alias_methodalias_method链技术使用无用的old_fooold_bar方法污染命名空间。 我喜欢我的命名空间干净。

我刚刚解决了上面提到的一些限制,并添加了一些其他function,但我懒得重写我的解释,所以我在这里重新发布修改后的版本:

 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 

JörgWMittag的解决方案非常好。 如果你想要更强大的东西(经过充分测试),那么最好的资源就是rails回调模块。