1. BeanMap
学习具体的技术工具的好办法就是些Demo、造轮子。所以,我实现了一个称为BeanMap的类来应用java反射API。
这个BeanMap的功能是将一个Bean包装成Map来使用。对调用者来说,是以操作Map的方式来操作BeanMap,
但是,实际上的数据是存储在被包装的Bean对象中的。
这种思路类似适配器模式,可以让你以Map的接口操作Bean对象。
但又有点像“视图”思想,真正的数据是存储在Bean对象中的,BeanMap只是对它进行操作的“视图”。对BeanMap的所有操作都会反映在后面的Bean对象中。
下面是BeanMap的一个使用例子。
|
|
示例中的Point这个Bean类拥有属性x和y。但是被BeanMap包装后,它就变成了一个拥有键”x”和键”y”的Map了。
那这个BeanMap类有什么实际应用呢?哈哈这只是我为了写反射的DEMO自己设计出来的一个类。
2. java反射
稍微思考下不难发现,BeanMap实现的关键点在于,
BeanMap接受外部传入的键,这是一个字符串。之后,它得到找到Bean对象中对应的getter和setter,并操作Bean对象。
将这个要求向通用化的方向分析,也即提供一个与函数匹配的字符串,得到对该函数的引用。
通过反射,就能够实现上面的要求。
现在流行的语言大都支持反射。反射能够在运行时得到程序元数据,比如某类的信息。
还能够根据这些元信息来修改程序状态或逻辑。
由于反射是在 运行 时得到的信息,那么支持反射的语言也必然要在程序运行时将这些元信息存放在内存某处。
java语言提供了反射API,这里是官方完整的文档:https://docs.oracle.com/javase/tutorial/reflect/ 。
对于反射出来的信息,Java的反射API将其以类的形式包装提供。
Java的反射机制提供了4个类:
- Class 类
- Constructor 构造器
- Field 属性
- Method 方法
现在,试图利用反射API,得到一个POJO类的所有属性名称。如下:
- 上述代码的思路很简单,java提供的反射API,能够得到该类的所有方法列表。按照约定,POJO类的属性xxx的getter方法都命名为GetXxx。那么,遍历这个表,找出所有getter,字符串处理下,就得到了所有的属性。
- 通过调用对象的getClass()方法得到一个Class对象,也即反射出该对象的Class信息。Class::getMethods函数则是反射出该类的所有方法。
- Method::getModifiers得到方法的修饰符信息。它是一个整数,我猜测是用位标记各个修饰符的,处理它需要用到位运算。不过可以使用Modifier这个类的工具方法去处理它。
不过,这里是想实现BeanMap。一种思路是,通过对反射出的列表进行一次处理,除了得到每个属性的名称外,还要得到它们的getter和setter。下面是一种粗暴的实现:
当然,上面的实现思路缺点很多。
首先,是性能问题。上面的代码虽然能实现功能,但是太暴力了。
虽然一个POJO类的方法顶多几十个,但是考虑到在具体的实践中,这些都是作为较为底层的基础设施,项目中可能会频繁被业务代码调用,因此对其进行性能上的分析是有必要的。
其次,上面这段逻辑考虑的也不全面。上面的逻辑是对公开的getter进行进一步的处理的,那么,如果getter是static的呢?
如果getter被native修饰呢?如果是超类所拥有的属性,那么该如何处理这种情况呢?
3. java内省API
为了方便使用,java针对反射API进行了封装,提供了一组内省API。
这组内省API主要是针对POJO类进行操作的,能够获取POJO类的属性信息。
那么,有了jdk自带的用于对Bean进行反射的工具后,上面的逻辑既可以简化了:
- 使用
getBeanInfo
方法获取某个Bean
类的内省信息,这些信息封装在BeanInfo
对象中。 PropertyDescriptor
是对Bean类中的一个属性的封装,通过它可以获取该属性的名称、getter方法、setter方法等信息。beanInfo::getPropertyDescriptors
获取Bean类的所有的PropertyDescriptor
。
可以看到,通过java的内省机制,解决了BeanMap的最关键的问题。而且,使用java自带的内省机制比自己通过反射API处理有以下好处:
- 内省API基于反射API进行的封装,使用更高层次的接口,当然更省心,开发效率更高。
- jdk在封装反射API的时候,会充分考虑到各种情况。如考虑到继承这一问题,Introspector::getBeanInfo函数可接收第二个参数,只反射出继承链中该类到第二个参数指定类之间类的属性。
- 内省API也充分考虑到了性能,其中拥有缓存机制,以提升性能。
4. 最后
解决了最关键的逻辑后,剩下的部分,就是对Map接口进行实现,填充一些封装目的的代码。在这里,我将核心的逻辑放在BeanMapImpl类中,而BeanMap仅仅负责实现Map接口,相关操作转发到BeanMapImpl相应的方法中实现。这样显得程序结构更为清晰:
https://github.com/frapples/java-wheel/blob/master/src/main/java/io/github/frapples/javawheel/reflection/BeanMapImpl.java