前提
這篇文章主要分析一下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基礎與進階》。