Ruby的class variable由于其奇怪的特性,一直是Ruby中包含争议的部分,大部分Rubist表示不使用class variable,最早我看到的文章是这篇写于2007年的帖子,里面的

@@avar = 1
class A
  @@avar = "hello"
end
puts @@avar  # => hello

成为了一个经典案例,至于其解释就是Ruby的class variable不属于owner类本身,而属于它的继承结构。@@avar实际定义在main所在类Object中,A继承自Object,因此也继承了@@avar这个class variable。

而昨天我又发现了另一个奇怪的特性

class A
  @@a = 1
  def f
    @@a
  end

  def f=(v)
    @@a = v
  end
end

A.new.f #=> 1
A.new.f = 2
A.new.f #=> 2

class A
  def self.f1
    @@a
  end

  class << self
    def f2
      @@a
    end
  end
end

A.f1 #=> 2
A.f2 #=> 2

# 直到这里,所有内容都可以理解
class << A
  def f3
    @@a
  end
end

A.f3 
#=> warning: class variable access from toplevel
#=> NameError: uninitialized class variable @@a in Object

这段代码的问题在于

class << A; ... ;end

class A; class << self; ... ; end; end

这两段代码本该是没有区别的,但是用后者访问的到A的class variable @@a而用前者却访问不到

当晚我把这段代码在Ruby Tuesday上提出过,也没人能回答我。回去以后仔细想想,可能觉得class variable对于gateway scope的敏感程度和其它两种变量类型local variable和instance variable不一样,在Ruby中,local variable不能穿越gateway scope存在,比如

a = 1
class A
  b = 2
  def f
    local_variables
  end
end
A.new.f #=> []

无论是a还是b,都没能进入到A的instance method f的定义中。而instance variable虽然也不能穿越gateway scope,但是等到gateway scope相同的时候却可以恢复出来

class A
  def self.f1
    @a = 1
  end

  def f2
    @a = 2
  end

  def self.f3
    @a
  end

  def f4
    @a
  end
end

A.f1
A.f3 #=> 1

a = A.new
a.f2
a.f4 #=> 2

但是class variable和它们恐怕都不一致,它是可以穿越gateway scope而存在的,比如

class A
  @@a = 1
  def f
    @@a
  end

  def f=(v)
    @@a = v
  end

  def self.f1
    @@a
  end
end

A.new.f #=> 1
A.new.f = 2
A.new.f #=> 2
A.f

可以看到,@@a在class A定义的类定义内就像全部变量一样的存在,轻松突破gateway scope,可以出现在类定义所在的每个角落。如果是这样的话,那就可以猜测了,class variable表现和其他变量类型不一致,对它而言只有class Xxxx的语句才是真正的gateway scope,其它语句统统ignore掉,包括class << Xxxxx在内。可以用以下代码证明:

class << A
  @@a = 1
end
@@a #=> 1
self.class #=> Object
self.class.class_variables #=> [:@@a]

看上去class variable定义在了A类中,但其实定义在了main所在类Object中。

我们夏季学期的作业,JavaEE的项目正在进行。在我的反复诱导下,Deltamaster 同学终于动用了AOP,不过很快就出了问题。他用AOP对所有Action类的execute方法进行增强(“advice”),但问题是,增强之后,View中的错误信息就无法输出了。

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<s:property value="errorMessage" />

由于AOP是我怂恿的,所以问题还是要由我来解决。Deltamaster如果有什么问题,那我肯定不会先去Google,因为我用Google就是他教的。所以首先,我仔细对比了启用AOP与不启用AOP两种情况下Action的表现,发现完全一致,确定Action没有问题。

由于AOP中我们用的是级别最高的around,有拦截并修改函数返回值的能力。我多次测试了around函数中proceed()函数的返回值在函数中各个点的位置,也没有发现问题。也就是说,around本身的代码也没有错误,但不排除是SpringFramework AOP的Bug使得Controller无法获取到action的返回值。不过,我暂时不会往Bug这块地方想。

如果Controller确实没有获取到action的返回值,也就是,jsp文件没有被调用。我随后在jsp后面加了一句输出语句,然后运行。输出成功了!这就表示该jsp文件确实被调用了,同时也证明了Controller确实获取到了action的返回值,前面AOP有Bug的推论不成立。总之从action到view,中间所有过程均正常。

那么就是errorMessage没有被获得了,在jsp中写errorMessage,实质将调用action类的getErrorMessage方法,我在这个方法中加了句输出语句。测试时这句话并没有输出,也就是说,getErrorMessage没有被调用。但为什么不被调用呢?我在action类中自己写了点get方法,测试发现都没有输出。(此处省略1000+字,关于我是如何验证是否是AOP的Bug造成getErrorMessage失效,以及是否是传回对象的类型无法匹配等等各种怀疑)最后,索性直接写

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<s:property value="class" />

输出了,而且发现是个proxy类,我随即将class改成了class.superclass.name,也就是输出本类的父类的名字,结果是,java.lang.reflect.Proxy,原因终于被发现了,作为一个Java的动态代理类,怎么可能有getErrorMessage方法。我们知道,SpringFramework AOP实现的核心方法是Java动态代理(基于接口的代理)或CGLIB(基于类的代理),这里似乎用的是前者。Proxy类对象代替Action类调用了View,结果当View要求调用者的getErrorMessage方法时,Proxy类没有这样的方法,所以导致出错!

随后我仔细查看了Proxy类的文档,全是类方法,没有任何能获得被代理对象的实例方法。无奈下,直接Google了,很快就找到了解答,很幸运,是中文解答:http://jeooo-li.iteye.com/blog/436931。“只要在Spring的配置文件applicationContext中的<aop:aspectj-autoproxy/>改为<aop:aspectj-autoproxy proxy-target-class="true"/>就可以了。”,经过测试,这个解答是正确的,问题解决。

经过初步测试,发现proxy-target-class="true" 一旦加上后,SpringFramework AOP将使用CGLIB方法而不是动态代理,生成Action类的子类,这样即可以满足AOP增强处理的要求,又完美继承了Action类的所有方法,因此才能解决问题。