我想拦截ruby-class上的方法调用,并且能够在实际执行方法之前和之后执行某些操作。我尝试了以下代码,但得到错误:
MethodInterception.rb:16:in
before_filter': (eval):2:in
alias_method':undefined methodsay_hello' for class
功课” (NameError) 来自(eval):2:在`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
答案 0 :(得分: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
方法中重命名了该方法,但是在您的客户端代码中,您在实际定义方法之前调用了before_filter
,从而导致了尝试重命名不存在的方法。
解决方案很简单:不要那样做!
好吧,好吧,也许不那么简单。您可能只是强迫您的客户在定义了他们的方法之后始终致电before_filter
。但是,这是糟糕的API设计。
因此,您必须以某种方式安排您的代码推迟方法的包装,直到它实际存在。这就是我所做的:不是在before_filter
方法中重新定义方法,而是仅记录以后要重新定义的事实。然后,我在method_added
挂钩中重新定义实际。
这里有一个小问题,因为如果你在method_added
内添加一个方法,那么当然会立即再次调用它并再次添加方法,这将导致它再次被调用,并且等等。所以,我需要防止递归。
请注意,此解决方案实际上还在客户端上执行排序:如果您在1}之后调用before_filter
,那么OP的版本仅有效em>定义方法,只有在之前调用时,我的版本才有效。但是,它很容易扩展,因此它不会遇到这个问题。
另请注意,我做了一些与问题无关的其他更改,但我认为更多的是Rubyish:
Module#define_method
代替eval
:eval
是邪恶的。 '努夫说。 (绝对没有理由在OP的代码中首先使用eval
。)alias_method
: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
答案 1 :(得分:2)
较少的代码从原来改变了。我只修改了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
答案 2 :(得分:0)
JörgWMittag的解决方案非常好。如果你想要更强大的东西(经过充分测试),那么最好的资源就是rails回调模块。