前提
這篇文章主要分析一下Introspector(內省,應該讀xing第三聲,沒有找到很好的翻譯,下文暫且這樣稱呼)的用法。Introspector是一個專門處理JavaBean的工具類,用來獲取JavaBean里描述符號,常用的JavaBean的描述符號相關類有BeanInfo、PropertyDescriptor,MethodDescriptor、BeanDescriptor、EventSetDescriptor和ParameterDescriptor。下面會慢慢分析這些類的使用方式,以及Introspector的一些特點。
JavaBean是什么
JavaBean是一種特殊(其實說普通也可以,也不是十分特殊)的類,主要用於傳遞數據信息,這種類中的方法主要用於訪問私有的字段,且方法名符合某種命名規則(字段都是私有,每個字段具備Setter和Getter方法,方法和字段命名滿足首字母小寫駝峰命名規則)。如果在兩個模塊之間傳遞信息,可以將信息封裝進JavaBean中,這種對象稱為值對象(Value Object)或者VO。這些信息儲存在類的私有變量中,通過Setter、Getter方法獲得。JavaBean的信息在Introspector里對應的概念是BeanInfo,它包含了JavaBean所有的Descriptor(描述符),主要有PropertyDescriptor,MethodDescriptor(MethodDescriptor里面包含ParameterDescriptor)、BeanDescriptor和EventSetDescriptor。
屬性Field和屬性描述PropertiesDescriptor的區別
如果是嚴格的JavaBean(Field名稱不重復,並且Field具備Setter和Getter方法),它的PropertyDescriptor會通過解析Setter和Getter方法,合並解析結果,最終得到對應的PropertyDescriptor實例。所以PropertyDescriptor包含了屬性名稱和屬性的Setter和Getter方法(如果存在的話)。
內省Introspector和反射Reflection的區別
Reflection:反射就是運行時獲取一個類的所有信息,可以獲取到類的所有定義的信息(包括成員變量,成員方法,構造器等)可以操縱類的字段、方法、構造器等部分。可以想象為鏡面反射或者照鏡子,這樣的操作是帶有客觀色彩的,也就是反射獲取到的類信息是必定正確的。Introspector:內省基於反射實現,主要用於操作JavaBean,基於JavaBean的規范進行Bean信息描述符的解析,依據於類的Setter和Getter方法,可以獲取到類的描述符。可以想象為"自我反省",這樣的操作帶有主觀的色彩,不一定是正確的(如果一個類中的屬性沒有Setter和Getter方法,無法使用Introspector)。
常用的Introspector相關類
主要介紹一下幾個核心類所提供的方法。
Introspector
Introspector類似於BeanInfo的靜態工廠類,主要是提供靜態方法通過Class實例獲取到BeanInfo,得到BeanInfo之后,就能夠獲取到其他描述符。主要方法:
public static BeanInfo getBeanInfo(Class<?> beanClass):通過Class實例獲取到BeanInfo實例。
BeanInfo
BeanInfo是一個接口,具體實現是GenericBeanInfo,通過這個接口可以獲取一個類的各種類型的描述符。主要方法:
BeanDescriptor getBeanDescriptor():獲取JavaBean描述符。EventSetDescriptor[] getEventSetDescriptors():獲取JavaBean的所有的EventSetDescriptor。PropertyDescriptor[] getPropertyDescriptors():獲取JavaBean的所有的PropertyDescriptor。MethodDescriptor[] getMethodDescriptors():獲取JavaBean的所有的MethodDescriptor。
這里要注意一點,通過BeanInfo#getPropertyDescriptors()獲取到的PropertyDescriptor數組中,除了Bean屬性的之外,還會帶有一個屬性名為class的PropertyDescriptor實例,它的來源是Class的getClass方法,如果不需要這個屬性那么最好判斷后過濾,這一點需要緊記,否則容易出現問題。
PropertyDescriptor
PropertyDescriptor類表示JavaBean類通過存儲器(Setter和Getter)導出一個屬性,它應該是內省體系中最常見的類。主要方法:
synchronized Class<?> getPropertyType():獲得屬性的Class對象。synchronized Method getReadMethod():獲得用於讀取屬性值(Getter)的方法;synchronized Method getWriteMethod():獲得用於寫入屬性值(Setter)的方法。int hashCode():獲取對象的哈希值。synchronized void setReadMethod(Method readMethod):設置用於讀取屬性值(Getter)的方法。synchronized void setWriteMethod(Method writeMethod):設置用於寫入屬性值(Setter)的方法。
舉個例子:
public class Main {
public static void main(String[] args) throws Exception {
BeanInfo beanInfo = Introspector.getBeanInfo(Person.class);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
if (!"class".equals(propertyDescriptor.getName())) {
System.out.println(propertyDescriptor.getName());
System.out.println(propertyDescriptor.getWriteMethod().getName());
System.out.println(propertyDescriptor.getReadMethod().getName());
System.out.println("=======================");
}
}
}
public static class Person {
private Long id;
private String name;
private Integer age;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
}
輸出結果:
age
setAge
getAge
=======================
id
setId
getId
=======================
name
setName
getName
=======================
不正當使用Introspector會導致內存溢出
如果框架或者程序用到了JavaBeans Introspector,那么就相當於啟用了一個系統級別的緩存,這個緩存會存放一些曾加載並分析過的Javabean的引用,當Web服務器關閉的時候,由於這個緩存中存放着這些Javabean的引用,所以垃圾回收器不能對Web容器中的JavaBean對象進行回收,導致內存越來越大。還有一點值得注意,清除Introspector緩存的唯一方式是刷新整個緩存緩沖區,這是因為JDK沒法判斷哪些是屬於當前的應用的引用,所以刷新整個Introspector緩存緩沖區會導致把服務器的所有應用的Introspector緩存都刪掉。Spring中提供的org.springframework.web.util.IntrospectorCleanupListener就是為了解決這個問題,它會在Web服務器停止的時候,清理一下這個Introspector緩存,使那些Javabean能被垃圾回收器正確回收。
也就是說JDK的Introspector緩存管理是有一定缺陷的。但是如果使用在Spring體系則不會出現這種問題,因為Spring把Introspector緩存的管理移交到Spring自身而不是JDK(或者在Web容器銷毀后完全不管),在加載並分析完所有類之后,會針對類加載器對Introspector緩存進行清理,避免內存泄漏的問題,詳情可以看CachedIntrospectionResults和SpringBoot刷新上下文的方法AbstractApplicationContext#refresh()中finally代碼塊中存在清理緩存的方法AbstractApplicationContext#resetCommonCaches();。但是有很多程序和框架在使用了JavaBeans Introspector之后,都沒有進行清理工作,比如Quartz、Struts等,這類操作會成為內存泄漏的隱患。
小結
- 在標准的
JavaBean中,可以考慮使用Introspector體系解析JavaBean,主要是方便使用反射之前的時候快速獲取到JavaBean的Setter和Getter方法。 - 在
Spring體系中,為了防止JDK對內省信息的緩存無法被垃圾回收機制回收導致內存溢出,主要的操作除了可以通過配置IntrospectorCleanupListener預防,還有另外一種方式,就是通過CachedIntrospectionResults類自行管理Introspector中的緩存(這種方式才是優雅的方式,這樣可以避免刷新整個Introspector的緩存緩沖區而導致其他應用的Introspector也被清空),也就是把JDK自行管理的Introspector相關緩存交給Spring自己去管理。在SpringBoot刷新上下文的方法AbstractApplicationContext#refresh()中finally代碼塊中存在清理緩存的方法AbstractApplicationContext#resetCommonCaches();,里面調用到的CachedIntrospectionResults#clearClassLoader(getClassLoader())方法就是清理指定的ClassLoader下的所有Introspector中的緩存的引用。
(本文完 e-a-20200811 c-1-d)

這是公眾號《Throwable》發布的原創文章,收錄於專輯《Java基礎與進階》。
