前言
在日常测试中,我们往往都有如果能获取被测对象的某个属性就方便多了的感慨,不幸的是大多数时候该属性都是private的,让我们望属性而兴叹。
不修改源代码而突破private成了很多qa的愿望,本文正是抛砖引玉的解决了这个问题。
JAVA篇
JAVA语言中有个非常有名的特性:Reflection。这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,当然包括类的私有成员变量和方法。
附件中的PrivateOperator.java对reflect进行了封装,可以方便地get、set私有成员方法 & 调用私有成员方法,甚至是父类的私有成员变量和方法!
get私有成员变量
这个需求可以使用PrivateOperator.getObjectProperty()。
方法声明:
/**
* @param owner: target object
* @param classLevel: 0 means itself, 1 means it's father, and so on...
* @param fieldName: name of the target field
* @return value of the target field
*/
public static Object getObjectProperty(Object owner, int classLevel, String fieldName)
用法举例:
Test t = new Test();
// get value of itself
System.out.println(PrivateOperator.getObjectProperty(t, 0, "privateString"));
// test get Parent private field
System.out.println(PrivateOperator.getObjectProperty(t, 1, "parentPrivate"));
set私有成员变量
这个需求可以使用PrivateOperator.setObjectProperty()。
方法声明:
/**
* @param owner: target object
* @param classLevel: 0 means itself, 1 means it's father, and so on...
* @param fieldName: name of the target field
* @param value: new value of the target field
*/
public static void setObjectProperty(Object owner, int classLevel, String fieldName, Object value)
用法举例:
Test t = new Test();
PrivateOperator.setObjectProperty(t, 0, "privateString", "I have been set");
t.printPrivateString();
调用私有成员方法
这个需求可以使用PrivateOperator.invokeObjectFunction()。
方法声明:
/**
* @param owner: target object
* @param classLevel: 0 means itself, 1 means it's father, and so on...
* @param methodName: name of the target method
* @param parameterTypes: types of the target method's parameters
* @param parameters: parameters of the target method
* @return result of invoked method
*/
public static Object invokeObjectMethod(Object owner, int classLevel, String methodName, Class[] parameterTypes, Object[] parameters)
用法举例:
Test t = new Test();
Class[] parameterTypes = {String.class, int.class};
Object[] parameters = {"from PrivateOperator", 1};
Object ret = PrivateOperator.invokeObjectMethod(t, 0, "testParameter", parameterTypes, parameters);
System.out.println(ret);
C++篇
看过了前面的解决方案,让人觉得突破private在JAVA简直如探囊取物,不过由于C++不支持反射,所以就没那么简单了。
系好安全带,让我们开始这段对象内存结构之旅吧。
解决方案0:编译选项
最容易想到的方法莫过于在目标类的首部加上#define private public,但是这种修改了源代码的方法是不提倡的。
在编译的时候加上-Dprivate=public,编译器会把所有private的成员的访问权限设为public。
修改编译选项这种方法不失为一种简便的突破private的方法,不过代价是要重新编译目标类。
如果不修改目标类、甚至是编译选项,那么还有其他方法可以突破private访问权限的限制吗?答案是有的
解决方案1:指针偏移
某种程度上,类的一个对象可以看作包含不同类型元素的数组,其数据成员的地址偏移由数据成员在类定义中的顺序决定. 其中类对象的地址指向类中第一个被定义的数据成员的地址;第二个被定义的数据成员的地址取决于第一个数据成员的类型,若第一个为 int 型,则再偏移 4 个字节( sizeof(int) )即得到第二个数据成员的地址(有时也不一定是这样,如下例中,由于类型对齐的缘故,实际偏移 8 个字节( sizeof(double) )才得到第二个数据成员的地址)。
细心的同学已经注意到示例代码中有一个被注释掉的虚函数声明,这是因为当类中有虚函数时对象的首地址会被插入虚函数表指针,即首个元素的地址会偏移sizeof(ptr),这个偏移量和类的成员变量类型对齐,即如果成员中都是4字节的变量,那么首地址偏移4字节;如果成员中有8字节的变量,同理首地址偏移8字节。如下图所示。
就像上面讨论的一样,这种方法虽然能突破private的限制,但是要考虑实际对象的内存结构,比如虚函数、字节对齐甚至是编译器对对象结构产生的影响,实际使用甚是不方便。 C++标准要求,在同一个access section(也就是private、public、protected等区段)中,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条即可(请看C++ Standard 9.2节)。也就是说各个members并不一定得连续排列(具体实现看编译器的喜好,实验显示VC6 & g++4.4都是不连续的),members中间可能被编译器由于类型对齐的原因补充一些bytes。
解决方案2:同构类
构造一个和目标类同构的类,这样它们的内存结构就是一致的,唯一不同的是这个辅助的类的成员都是public的,这样将目标对象转换为同构类型再读取私有成员,就可以躲开编译器的类型检查。
具体做法:将目标类代码粘贴到struct中(非虚函数可省),去除访问限制符号。访问目标对象时将其转换为同构类型即可
示例代码如下:
使用这种简单的方法,甚至可以访问目标对象的私有成员方法!
局限性:由于子类不能访问父类的私有成员变量和函数,所以这种构造同构类的方法也不能访问父类的私有成员变量和函数。