1. 程式人生 > >聊聊Java內省Introspector

聊聊Java內省Introspector

## 前提 這篇文章主要分析一下`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`)的方法。 舉個例子: ```java 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; } } } ``` 輸出結果: ```shell 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) ![](https://public-1256189093.cos.ap-guangzhou.myqcloud.com/static/wechat-account-logo.png) 這是公眾號《Throwable》釋出的原創文章,收錄於專輯《Java基礎與進