java内省机制及PropertyUtils使用方法

时间:2022-06-01 18:53:32

背景

一般情况下,在Java中你可以通过get方法轻松获取beans中的属性值。但是,当你事先不知道beans的类型或者将要访问或修改的属性名时,该怎么办?Java语言中提供了一些像java.beans.Introspector这 样类,实现了在运行时检测Java类并确定属性get和set方法的名称,结合Java中的反射机制就可以调用这些方法了。然而,这些APIs使用起来比 较困难,并且将Java类中一些不必要的底层结构暴露给了开发人员。BeanUtils包中的APIs试图简化动态获取和设置bean属性的过程。

BeanUtils包中的PropertyUtils类中的一些静态方法实现了上面的功能,稍后会详细介绍。首先,介绍一些有用的定义:

JavaBean支持的属性类型一般可以划分成三类--标准的JavaBeans规范支持其中的一些,也有一部分只有BeanUtils包支持:

  • Simple(单值)  --  单值或量,有个一可以访问或修改的属性。值的类型可能是Java语言的原生类型(如:int型),简单的类(如:java.lang.String),或者一个复杂类的对象,这个类可能来自Java语言或者来自应用程序再或者来自应用程序中的一个类库。
  • Indexed(索 引)  --   索引的属性,属性中存放有序对象(都是同类型的)的集合,每个对象都可以通过一个非负的整数值(或下标)来获取。另外,所有值的集合可以使用一个数组来设 置或者获取。作为一个JavaBeans规范的扩展,BeanUtils包认为所有底层数据类型为java.util.List(或List的一个实现) 的属性都可以被索引。
  • Mapped(映射)  --  作为一个标准JavaBeans APIs的扩展,  BeanUtils包认为所有底层数据类型为java.util.Map的属性都可以被"映射"。你可以通过String类型的key值来设置或者获取对应的值。

PropertyUtils类中提供了各种API方法用来获取和设置上述三种类型的属性。在下面的程序片段中,假设在bean类中都定义了如下的方法:

  1. public class Employee {
  2. public Address getAddress(String type);
  3. public void setAddress(String type, Address address);
  4. public Employee getSubordinate(int index);
  5. public void setSubordinate(int index, Employee subordinate);
  6. public String getFirstName();
  7. public void setFirstName(String firstName);
  8. public String getLastName();
  9. public void setLastName(String lastName);

访问基本属性
      获取和设置simple属性很简单。在Javadocs中查看下面两个方法:

  • PropertyUtils.getSimpleProperty(Object bean, String name)
  • PropertyUtils.setSimpleProperty(Object bean, String name, Object value)

使用这两个方法,你可以动态地修改employee的name属性:

  1. Employee employee = ...;
  2. String firstName = (String) PropertyUtils.getSimpleProperty(employee, "firstName");
  3. String lastName = (String) PropertyUtils.getSimpleProperty(employee, "lastName");
  4. ... manipulate the values ...
  5. PropertyUtils.setSimpleProperty(employee, "firstName", firstName);
  6. PropertyUtils.setSimpleProperty(employee, "lastName", lastName);

对于indexed(索引)属性,你有两种选择 - 你既可以在属性名后面添加方括号在里面放上一个下标,也可以在调用方法时将其作为一个独立参数:

  • PropertyUtils.getIndexedProperty(Object bean, String name)
  • PropertyUtils.getIndexedProperty(Object bean, String name, int index)
  • PropertyUtils.setIndexedProperty(Object bean, String name, Object value)
  • PropertyUtils.setIndexedProperty(Object bean, String name, int index, Object value)
      属性名的下标只能是整数常量。如果你想获取的项的索引是计算出来的,你可以将属性名和索引作为字符串组合起来。例如,你可以向下面这样做:
    Employee employee = ...;
    int index = ...;
    String name = "subordinate[" + index + "]";
    Employee subordinate = (Employee) PropertyUtils.getIndexedProperty(employee, name);

Employee employee = ...;
    int index = ...;
    Employee subordinate = (Employee) PropertyUtils.getIndexedProperty(employee, "subordinate", index);

类似的,获取和设置mapped(映射)属性的方法同样有两对。与indexed(索引)不同的是额外的属性是用括号括起来的(“(”和“)”)而不是方括号,并且获取和设置值时如同从底层的map中获取和设置值一样。

  • PropertyUtils.getMappedProperty(Object bean, String name)
  • PropertyUtils.getMappedProperty(Object bean, String name, String key)
  • PropertyUtils.setMappedProperty(Object bean, String name, Object value)
  • PropertyUtils.setMappedProperty(Object bean, String name, String key, Object value)

例如,你可以使用下面两种方法设置employee的家庭住址:

Employee employee = ...;
    Address address = ...;
    PropertyUtils.setMappedProperty(employee, "address(home)", address);

Employee employee = ...;
    Address address = ...;
    PropertyUtils.setMappedProperty(employee, "address", "home", address);

访问嵌套属性
      在上面的例子中,我们假设你将bean作为第一个参数传入PropertyUtils方法,并希望获取指定属性的值。然而,如果属性的值是一个Java对象,并且你希望进一步获取这个Java对象的某个属性的值?

      例如,假设你事实上想要获取的值是employee家庭住址中的city属性。使用标准的Java编程技术直接获取bean的对应属性,你可以这样写:

String city = employee.getAddress("home").getCity();

使用PropertyUtils类中的等效机制被称为嵌套属性访问。使用这种方法,你将访问路径上的属性的名称用“.”拼接起来 --与你在JavaScript执行嵌套属性访问的方式非常相似。

  • PropertyUtils.getNestedProperty(Object bean, String name)
  • PropertyUtils.setNestedProperty(Object bean, String name, Object value)
      PropertyUtils中等效于上面的Java代码将是这样:

String city = (String) PropertyUtils.getNestedProperty(employee, "address(home).city");

最后,方便起见,PropertyUtils提供了如下一组方法,它们接收simple、indexed和mapped属性的任意组合方法,支持任意层次的嵌套:

  • PropertyUtils.getProperty(Object bean, String name)
  • PropertyUtils.setProperty(Object bean, String name, Object value)
      你可以像这样使用:

Employee employee = ...;
    String city = (String) PropertyUtils.getProperty(employee,"subordinate[3].address(home).city");

二、java反射和内省
概述;
1.什么是反射
反射就是在运行状态把 Java 类中的各种成分映射成相应相应的 Java 类,可以动态得获取所有的属性以及动态调用任意一个方法。
1).一段java代码在程序的运行期间会经历三个阶段:source-->class-->runtime
2).Class对象
在java中用一个Class对象来表示一个java类的class阶段
Class对象封装了一个java类定义的成员变量、成员方法、构造方法、包名、类名等。
2.反射怎么用
1).获得java类的各个组成部分,首先需要获得代表java类的Class对象
获得Class对象有以下三种方式:
Class.forname(className) 用于做类加载
obj.getClass() 用于获得对象的类型
类名.class 用于获得指定的类型,传参用
2).反射类的构造方法,获得实例
Class clazz = 类名.class;
Constuctor con = clazz.getConstructor(new Class[]{paramClazz1,paramClazz2,.....});
con.newInstance(params....);
3).反射类的成员方法
Method m = clazz.getMethod(methodName,new Class[]{paramClazz1,paramClazz2,.....});
m.invoke();
4).反射类的属性
Field field = clazz.getField(fieldName);
field.setAccessible(true);//设置为可访问
filed.setObject(value); //设置值
Object value = field.get(clazz); //获得值
Object staticValue = filed.get(Class); //获得静态值

二:内省
1.什么是内省
通过反射的方式操作JavaBean的属性,jdk提供了PropertyDescription类来操作访问JavaBean的属性,Beantils工具基于此来实现。
2.内省怎么用
1).操作一个属性
Object obj = new Object();
PropertyDescriptor pd = new PropertyDescriptor(propertyName,Class); //声明属性描述对象,一次只可描述一个属性
Method m = pd.getWriterMethod();//获取setter方法
m.invoke(obj,value);
Method m = pd.getReaderMethod();//获取getter方法
Object value = m.invoke(obj);
2).操作多个属性
BeanInfo bi = Instospector.getBeanInfo(beanClass);//获取Bean描述对象
PropertyDescriptor[] pds = bi.getPropertyDescriptors();//获取属性描述对象数组
拿到属性描述对象数组之后再循环数组,剩余的操作就跟"操作一个属性"相同了。

反射

相对而言,反射比内省更容易理解一点。用一句比较白的话来概括,反射就是让你可以通过名称来得到对象(类,属性,方法)的技术。例如我们可以通过类
名来生成一个类的实例;知道了方法名,就可以调用这个方法;知道了属性名就可以访问这个属性的值,还是写两个例子让大家更直观的了解反射的使用方法:

  1. //通过类名来构造一个类的实例
  2. ClassClasscls_str=Class.forName("java.lang.String");
  3. //上面这句很眼熟,因为使用过JDBC访问数据库的人都用过J
  4. Objectstr=cls_str.newInstance();
  5. //相当于Stringstr=newString();
  6. //通过方法名来调用一个方法
  7. StringmethodName="length";
  8. Methodm=cls_str.getMethod(methodName,null);
  9. System.out.println("lengthis"+m.invoke(str,null));
  10. //相当于System.out.println(str.length());

上面的两个例子是比较常用方法。看到上面的例子就有人要发问了:为什么要这么麻烦呢?本来一条语句就完成的事情干吗要整这么复杂?没错,在上面的例 子中确实没有必要这么麻烦。不过你想像这样一个应用程序,它支持动态的功能扩展,也就是说程序不重新启动但是可以自动加载新的功能,这个功能使用一个具体 类来表示。首先我们必须为这些功能定义一个接口类,然后我们要求所有扩展的功能类必须实现我指定的接口,这个规定了应用程序和可扩展功能之间的接口规则, 但是怎么动态加载呢?我们必须让应用程序知道要扩展的功能类的类名,比如是test.Func1,当我们把这个类名(字符串)告诉应用程序后,它就可以使 用我们第一个例子的方法来加载并启用新的功能。这就是类的反射,请问你有别的选择吗?

内省

内省是Java语言对Bean类属性、事件的一种缺省处理方法。例如类A中有属性name,那我们可以通过getName,setName来得到其 值或者设置新的值。通过getName/setName来访问name属性,这就是默认的规则。Java中提供了一套API用来访问某个属性的 getter/setter方法,通过这些API可以使你不需要了解这个规则,这些API存放于包java.beans中。

一般的做法是通过类Introspector来获取某个对象的BeanInfo信息,然后通过BeanInfo来获取属性的描述器 (PropertyDescriptor),通过这个属性描述器就可以获取某个属性对应的getter/setter方法,然后我们就可以通过反射机制来 调用这些方法。下面我们来看一个例子,这个例子把某个对象的所有属性名称和值都打印出来:

  1. /*
  2. *Createdon2004-6-29
  3. */
  4. packagedemo;
  5. importjava.beans.BeanInfo;
  6. importjava.beans.Introspector;
  7. importjava.beans.PropertyDescriptor;
  8. publicclassIntrospectorDemo{
  9. Stringname;
  10. publicstaticvoidmain(String[]args)throwsException{
  11. IntrospectorDemodemo=newIntrospectorDemo();
  12. demo.setName("WinterLau");
  13. //如果不想把父类的属性也列出来的话,
  14. //那getBeanInfo的第二个参数填写父类的信息
  15. BeanInfobi=Introspector.getBeanInfo(demo.getClass(),Object.class);
  16. PropertyDescriptor[]props=bi.getPropertyDescriptors();
  17. for(inti=0;i<props.length;i++){
  18. System.out.println(props[i].getName()+"="+
  19. props[i].getReadMethod().invoke(demo,null));
  20. }
  21. }
  22. publicStringgetName(){
  23. returnname;
  24. }
  25. publicvoidsetName(Stringname){
  26. this.name=name;
  27. }
  28. }

Web开发框架Struts中的FormBean就是通过内省机制来将表单中的数据映射到类的属性上,因此要求FormBean的每个属性要有 getter/setter方法。但也并不总是这样,什么意思呢?就是说对一个Bean类来讲,我可以没有属性,但是只要有getter/setter方 法中的其中一个,那么Java的内省机制就会认为存在一个属性,比如类中有方法setMobile,那么就认为存在一个mobile的属性,这样可以方便 我们把Bean类通过一个接口来定义而不用去关心具体实现,不用去关心Bean中数据的存储。比如我们可以把所有的getter/setter方法放到接 口里定义,但是真正数据的存取则是在具体类中去实现,这样可提高系统的扩展性。

总结

将Java的反射以及内省应用到程序设计中去可以大大的提供程序的智能化和可扩展性。有很多项目都是采取这两种技术来实现其核心功能,例如我们前面 提到的Struts,还有用于处理XML文件的Digester项目,其实应该说几乎所有的项目都或多或少的采用这两种技术。在实际应用过程中二者要相互 结合方能发挥真正的智能化以及高度可扩展性。

三、缺点及优化

在web.xml中注册IntrospectorCleanupListener监听器以解决struts等框架可能产生的内存泄露问题

增加方式如下:

  1. <listener>
  2. <listener-class>
  3. org.springframework.web.util.IntrospectorCleanupListener
  4. </listener-class>
  5. </listener>
  1. <listener>
  2. <listener-class>
  3. org.springframework.web.util.IntrospectorCleanupListener
  4. </listener-class>
  5. </listener>

org.springframework.web.util.IntrospectorCleanupListener源代码中对其的解释如下:

        Listener
that flushes the JDK's JavaBeans Introspector cache on web app
shutdown. Register this listener in your web.xml to guarantee proper
release of the web application class loader and its
loaded classes.

在Web应用程序关闭时IntrospectorCleanupListener将会刷新JDK的JavaBeans的Introspector缓存。在
你的web.xml中注册这个listener来确保Web应用程序的类加载器以及其加载的类正确的释放资源。

        If
the JavaBeans Introspector has been used to analyze application classes,
the system-level Introspector cache will hold a hard reference to those
classes. Consequently, those classes and the
web application class loader will not be garbage-collected on web app
shutdown! This listener performs proper cleanup, to allow for garbage
collection to take effect.

如果JavaBeans的Introspector已被用来分析应用程序类,系统级的Introspector缓存将持有这些类的一
个硬引用。因此,这些类和Web应用程序的类加载器在Web应用程序关闭时将不会被垃圾收集器回收!而
IntrospectorCleanupListener则会对其进行适当的清理,已使其能够被垃圾收集器回收。

Unfortunately,
the only way to clean up the Introspector is to flush the entire cache,
as there is no way to specifically determine the application's classes
referenced there. This will remove
cached introspection results for all other applications in the server
too.

       不幸的是,唯一能够清理Introspector的方法是刷新整个Introspector缓存,没有其他办法来确切指定应用程序所引用的类。这将删除所有其他应用程序在服务器的缓存的Introspector结果。

       spring's
beans infrastructure within the application, as Spring's own
introspection results cache will immediately flush an analyzed class
from the JavaBeans Introspector cache and only hold a cache within the
application's own ClassLoader. Although Spring itself does not create
JDK Introspector leaks, note that this listener should nevertheless be
used in scenarios where the Spring framework
classes themselves reside in a 'common' ClassLoader (such as the system
ClassLoader). In such a scenario, this listener will properly clean up
Spring's introspection cache.

请注意,在使用Spring内部的bean机制时,不需要使用此监听器,因为Spring自己的introspection
results cache将会立即刷新被分析过的JavaBeans Introspector
cache,而仅仅会在应用程序自己的ClassLoader里面持有一个cache。虽然Spring本身不产生泄漏,注意,即使在Spring框架的
类本身驻留在一个“共同”类加载器(如系统的ClassLoader)的情况下,也仍然应该使用使用
IntrospectorCleanupListener。在这种情况下,这个IntrospectorCleanupListener将会妥善清理
Spring的introspection
cache。

       Application
classes hardly ever need to use the JavaBeans Introspector directly, so
are normally not the cause of Introspector resource leaks. Rather, many
libraries and frameworks do not clean
up the Introspector: e.g. Struts and Quartz.

应用程序类,几乎不需要直接使用JavaBeans Introspector,所以,通常都不是Introspector resource造成内存泄露。相反,许多库和框架,不清理Introspector,例如: Struts和Quartz。

       Note
that a single such Introspector leak will cause the entire web app class
loader to not get garbage collected! This has the consequence that you
will see all the application's static class
resources (like singletons) around after web app shutdown, which is not
the fault of those classes!

需要注意的是一个简单Introspector泄漏将会导致整个Web应用程序的类加载器不会被回收!这样做的结果,将会是在web应用程序关闭时,该应
用程序所有的静态类资源(比如:单实例对象)都没有得到释放。而导致内存泄露的根本原因其实并不是这些未被回收的类!

This listener should be registered as the first one in web.xml, before any application listeners
such as Spring's ContextLoaderListener. This allows the listener to take full effect at the right time of the lifecycle.

IntrospectorCleanupListener应该注册为web.xml中的第一个Listener,在任何其他
Listener之前注册,比如在Spring's
ContextLoaderListener注册之前,才能确保IntrospectorCleanupListener在Web应用的生命周期适当时机
生效。