Introspector作用及影響
在分析IntrospectorCleanupListener之前,先了解一下Introspector。Introspector是JDK中java.beans包下的類,它為目標JavaBean提供了一種了解原類方法、屬性和事件的標準方法。通俗的說,就是可以通過Introspector構建一個BeanInfo對象,而這個BeanInfo對象中包含了目標類中的屬性、方法和事件的描述信息,然后可以使用這個BeanInfo對象對目標對象進行相關操作。
下面看一個簡單的示例會很容易明白。為了簡單,Student類中只有一個name屬性。
結果輸出:Student{name='張三'}
通過查看Introspector.getBeanInfo方法的源碼會發現,Introspector在構建一個BeanInfo對象的時候,會將構建的BeanInfo對象和原類緩存到一個Map中,源碼如下。
通過上的代碼可以得出,Introspector間接持有了BeanInfo的強引用。如果使用Introspector操作了很多類,那么Introspector將間接持有這些BeanInfo的強引用。在發生垃圾收集的時候,檢測到這些BeanInfo存在引用鏈,則這些類和對應的類加載器將不會被垃圾收集器回收,進而導致內存泄漏。所以,為了解決這個問題,在使用Introspector操作完成后,調用Introspector類的flushCaches方法清除緩存。
通過上面的代碼會發現,清除的時候是清空了整個緩存,因為沒有很好的辦法來確定每個緩存是屬于哪個應用的,所以清除的時候會清除所有應用的緩存。
IntrospectorCleanupListener解析
上面分析了Introspector的作用和影響,那IntrospectorCleanupListener和Introspector有什么關系呢?
IntrospectorCleanupListener是spring-web jar中的類,源碼如下。
IntrospectorCleanupListener實現了ServletContextListener接口,也就是說,在web容器初始化(準確的說是在filters或servlets初始化之前)的時候會執行contextInitialized方法,在ServletContext銷毀(準確的說是在filters和servlets銷毀之后)的時候會執行contextDestroyed方法。從圖中contextDestroyed方法,可以看到在銷毀ServletContext的時候調用了Introspector.flushCaches方法,清空了對應緩存。IntrospectorCleanupListener中為什么要這么做?難道是Spring使用Introspector操作后沒有清空對應緩存?查看IntrospectorCleanupListener類的源碼,會發現有這樣一段標注。
大意是說,在使用Spring本身的時候并不需要使用此監聽器,因為Spring自己的內部機制會立即清空對應的緩存。雖然,Spring本身不存在這樣的問題,但是如果和其它框架結合使用,而其它框架有這個問題,如Struts、Quartz等,那就需要配置這個監聽器,在銷毀ServletContext的時候清空對應緩存。
有一點需要注意的是,像這樣一個簡單的Introspector內存泄漏將會導致整個應用的類加載器不會被垃圾收集器回收,如果有內存泄漏的問題,可以考慮此因素。
配置IntrospectorCleanupListener
在以往的工作經歷中,多次看到在web.xml中將IntrospectorCleanupListener配置成非第一個listener。
其實,看過源碼的都知道,官方的表述是必須將此監聽器配置成web.xml中的第一個listener,才能在合適的時間發揮最有效的作用。
原因其實很簡單,在Servlet3.0規范之前,監聽器的調用是隨機的,而從Servlet3.0開始,監聽器的調用順序是根據其在web.xml中配置的順序,并且實現ServletContextListener的監聽器,contextInitialized方法調用順序是按照在web.xml中配置的順序正序依次執行,而contextDestroyed方法的調用順序是按照在web.xml中配置的順序逆序依次執行。所以,如果IntrospectorCleanupListener被配置成了第一個listener,那么它的contextDestroyed方法將最后一個執行,將發揮最有效的清除作用;而如果不是,那么可能會殘留未被清除的緩存。