• Java开发笔记(七十九)利用反射技术操作私有属性


    早在介绍多态的时候,曾经提到公鸡实例的性别属性可能被篡改为雌性,不过面向对象的三大特性包含了封装、继承和多态,只要把性别属性设置为private私有级别,也不提供setSex这样的性别修改方法,那么性别属性就被严严实实地封装了起来,不但外部无法修改性别属性,连公鸡类的子类都无法修改。如此一来,公鸡实例的性别属性可谓防护周全,压根不存在被篡改的可能性。但是Java给面向对象留了个后门,也就是反射技术,利用反射技术竟然能够攻破封装的防护网,使得篡改私有属性从理想变成了现实,赶紧来看看反射技术是怎样做到这点的。
    上一篇文章讲到通过字符串可以获得该串所代表的Class对象,那么通过字段名称字符串也能获得对应的字段对象,其中的获取操作用到了Class对象的getDeclaredField方法,完整的字段对象获取代码如下所示:

    		try {
    			Class cls = Chicken.class; // 获得Chicken类的基因类型
    			// 通过字段名称获取该类的字段对象
    			Field sexField = cls.getDeclaredField("sex");
    		} catch (NoSuchFieldException e) { // 捕捉到了无此字段异常
    			e.printStackTrace();
    		} catch (SecurityException e) { // 捕捉到了安全异常
    			e.printStackTrace();
    		}
    

    注意调用getDeclaredField方法之时需要捕捉两种异常,包括无此字段异常NoSuchFieldException和安全异常SecurityException。现在得到的Field对象便隐藏着sex属性的内在信息,要想从Field对象挖掘出sex属性的数值,还得继续下列两个步骤的处理:
    1、调用Field对象的setAccessible方法,并传入true值,表示将该字段设置为允许访问,以解除private的限制;
    2、调用Field对象的getInt方法,并传入鸡类实例,表示准备从该示例中获取指定字段的整型值。同理调用getBoolean方法获取的是布尔值,调用getString方法获取的是字符串值。倘若是获取基本类型以外的类型值,则需先调用get方法获得Object对象,再强制转换为目标类型。
    整合以上的两个处理步骤,得到以下的字段数值获取代码:

    			if (sexField != null) {
    				sexField.setAccessible(true); // 将该字段设置为允许访问
    				try {
    					sex = sexField.getInt(chicken); // 获取某实例的字段值
    				} catch (IllegalArgumentException e) { // 捕捉到了非法参数异常
    					e.printStackTrace();
    				} catch (IllegalAccessException e) { // 捕捉到了非法入口异常
    					e.printStackTrace();
    				}
    			}
    

    注意字段对象的getInt方法在调用时也要捕捉两种异常,包括非法参数异常IllegalArgumentException,以及非法入口异常IllegalAccessException。这里的两种异常加上之前调用getDeclaredField方法的两种异常,寥寥数行的反射代码竟要手工捕捉四种异常,未免太大动干戈了。其实程序员可以相信自己,保证反射过程中的操作代码完全正确,这样便无需逐个捕捉某种异常,只要一次性捕捉总的异常即Exception就行了。于是简化了异常捕捉逻辑的反射代码变成了下面这般:

    	// 通过反射来获得某个实例的私有属性
    	private static int getReflectSex(Chicken chicken) {
    		int sex = -1;
    		try {
    			Class cls = Chicken.class; // 获得Chicken类的基因类型
    			// 通过字段名称获取该类的字段对象
    			Field sexField = cls.getDeclaredField("sex");
    			if (sexField != null) {
    				sexField.setAccessible(true); // 将该字段设置为允许访问
    				sex = sexField.getInt(chicken); // 获取某实例的字段值
    			}
    		} catch (Exception e) { // 捕捉到了任何一种异常(错误除外)
    			e.printStackTrace();
    		}
    		return sex;
    	}
    

    然而上面的代码仅仅通过反射取到性别字段的数值,仍旧没能修改该字段的数值,若想真正改变性别字段的取值,需要把getInt方法改为setInt方法,并给setInt方法的第二个参数传入修改后的数值。此时利用反射技术篡改字段值的代码示例如下:

    	// 通过反射来修改某个实例的私有属性
    	private static void setReflectSex(Chicken chicken, int sex) {
    		try {
    			Class cls = Chicken.class; // 获得Chicken类的基因类型
    			// 通过字段名称获取该类的字段对象
    			Field sexField = cls.getDeclaredField("sex");
    			if (sexField != null) {
    				sexField.setAccessible(true); // 将该字段设置为允许访问
    				sexField.setInt(chicken, sex); // 将某实例的该字段修改为指定数值
    			}
    		} catch (Exception e) { // 捕捉到了任何一种异常(错误除外)
    			e.printStackTrace();
    		}
    	}
    

    从上述的setReflectSex代码可知,该方法传入一个鸡类实例和新的性别,目的是把这只鸡的性别变过来。这下有了getReflectSex方法可读取性别属性,还有setReflectSex方法可写入性别属性,再由外部接连调用这两个方法,从而验证反射技术的执行效果。下面是外部先后篡改公鸡实例性别、篡改母鸡实例性别的演示代码:

    		Cock cock = new Cock(); // 创建一个公鸡实例
    		System.out.println("准备修理公鸡,性别取值 = "+getReflectSex(cock));
    		setReflectSex(cock, cock.FEMALE); // 把公鸡实例的性别篡改为“雌性”
    		System.out.println("结束修理公鸡,性别取值 = "+getReflectSex(cock));
    		Hen hen = new Hen(); // 创建一个母鸡实例
    		System.out.println("准备修理母鸡,性别取值 = "+getReflectSex(hen));
    		setReflectSex(hen, hen.MALE); // 把母鸡实例的性别篡改为“雄性”
    		System.out.println("结束修理母鸡,性别取值 = "+getReflectSex(hen));
    

    运行以上的演示代码,观察到下列的日志描述:

    准备修理公鸡,性别取值 = 0
    结束修理公鸡,性别取值 = 1
    准备修理母鸡,性别取值 = 1
    结束修理母鸡,性别取值 = 0
    

    可见尽管鸡类的sex属性被声明为private,但是公鸡实例的性别依然被篡改为雌性,母鸡实例的性别依然被篡改为雄性了。



    更多Java技术文章参见《Java开发笔记(序)章节目录

  • 相关阅读:
    between and 相关
    SQL获取所有用户名,数据库名、所有表名、所有字段名及字段类型
    性能优化探讨与分析:
    设置自动收缩数据库
    服务器注册
    多表查询及区别
    sql孤立用户解决方法
    委托、事件、观察者模式
    非托管资源
    C# 预处理器指令
  • 原文地址:https://www.cnblogs.com/pinlantu/p/10639325.html
Copyright © 2020-2023  润新知