Java內(nèi)省之Introspector解讀
Java內(nèi)省之Introspector
在JavaBean規(guī)范中有如下描述:
大意是java默認(rèn)情況下jdk使用低級的反射機制來分析Bean,為了方便其他人分析bean,java提供了一個內(nèi)省類Introspector,使用Introspector的getBeanInfo方法可以獲取一個封裝了bean信息(包括屬性和方法)的BeanInfo對象。
Introspector使用不當(dāng)導(dǎo)致內(nèi)存泄露的風(fēng)險
框架幾乎都使用了Introspector類來實現(xiàn)靈活性,但是Introspector在獲取beanInfo對象時,為了提高性能使用了緩存保存beanInfo:
public static BeanInfo getBeanInfo(Class<?> beanClass) ? ? ? ? throws IntrospectionException ? ? { ? ? ? ? if (!ReflectUtil.isPackageAccessible(beanClass)) { ? ? ? ? ? ? return (new Introspector(beanClass, null, USE_ALL_BEANINFO)).getBeanInfo(); ? ? ? ? } ? ? ? ? ThreadGroupContext context = ThreadGroupContext.getContext(); ? ? ? ? BeanInfo beanInfo; ? ? ? ? synchronized (declaredMethodCache) { ? ? ? ? ? ? beanInfo = context.getBeanInfo(beanClass); ? ? ? ? } ? ? ? ? if (beanInfo == null) { ? ? ? ? ? ? beanInfo = new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo(); ? ? ? ? ? ? synchronized (declaredMethodCache) { ? ? ? ? ? ? ? ? context.putBeanInfo(beanClass, beanInfo); ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? return beanInfo; ? ? }
緩存使用ThreadGroupContext——線程組級別共享,類似與ThreadLocal。
內(nèi)部使用WeakHashMap——key為弱引用來保存beanInfo,其中使用class作為key,beanInfo作為value。
同時使用WeakIdentityMap保存ThreadGroupContext對象(應(yīng)該是ThreadGroupContext對象的hash值)與WeakHashMap的映射關(guān)系,也就是說不同線程組相互隔離:
final class ThreadGroupContext { //WeakIdentityMap 判斷key是否重復(fù)只判斷hash是否相等,不調(diào)用equals ? ? private static final WeakIdentityMap<ThreadGroupContext> contexts = new WeakIdentityMap<ThreadGroupContext>() { ? ? ? ? protected ThreadGroupContext create(Object key) { ? ? ? ? ? ? return new ThreadGroupContext(); ? ? ? ? } ? ? }; ? ? /** ? ? ?* Returns the appropriate {@code ThreadGroupContext} for the caller, ? ? ?* as determined by its {@code ThreadGroup}. ? ? ?* ? ? ?* @return ?the application-dependent context ? ? ?*/ ? ? static ThreadGroupContext getContext() { ? ? ? ? return contexts.get(Thread.currentThread().getThreadGroup()); ? ? } 。 。 。 }
但是beanInfo中持有class對象,因此WeakHashMap的弱引用失效,Introspector提供了清除緩存的方法flushCaches。
但有些框架在使用Introspector之后并沒有清除緩存
在spring中有如下描述:
/** ?* Listener that flushes the JDK's {@link java.beans.Introspector JavaBeans Introspector} ?* cache on web app shutdown. Register this listener in your {@code web.xml} to ?* guarantee proper release of the web application class loader and its loaded classes. ?* ?* <p><b>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!</b> This listener performs proper cleanup, ?* to allow for garbage collection to take effect. ?* ?* <p>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. ?* ?* <p>Note that this listener is <i>not</i> necessary when using 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. ?* ?* <b>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).</b> ?* In such a scenario, this listener will properly clean up Spring's introspection cache. ?* ?* <p>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. ?* ?* <p>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! ?* ?* <p><b>This listener should be registered as the first one in {@code web.xml}, ?* before any application listeners such as Spring's ContextLoaderListener.</b> ?* This allows the listener to take full effect at the right time of the lifecycle.
大意是在web應(yīng)用中使用Introspector分析bean,當(dāng)web應(yīng)用停止時(這里應(yīng)該指的是正常銷毀,而非殺死進(jìn)程暴力銷毀),由于Introspector持有被分析bean的強引用,導(dǎo)致bean以及加載bean的classload無法被gc,造成內(nèi)存泄露。
個人猜想,如果web應(yīng)用停止之后,main方法運行結(jié)束,jvm退出應(yīng)該不存在內(nèi)存泄露的情況。但是,當(dāng)web服務(wù)銷毀之后main方法還在執(zhí)行,那么就出現(xiàn)內(nèi)存泄露。例如在一個tomcat中部署多個應(yīng)用,在tomcat的manager App 頁面關(guān)閉應(yīng)用就會導(dǎo)致內(nèi)存泄露。
大部分框架在創(chuàng)建線程池的時候都繼承parentThreadGroup,因此即使使用WeakIdentityMap保存ThreadGroup對象的軟引用與WeakHashMap的映射關(guān)系,但其他未關(guān)閉的web應(yīng)用仍然持有ThreadGroup的強引用,因此WeakIdentityMap中的beanInfo緩存不會被回收——內(nèi)存泄露。
IntrospectorCleanupListener
為了解決其他框架如:
Struts和Quartz(大部分博客均指出這兩個框架使用Introspector后沒有flushCaches,但我沒有考證),一心為我們考慮的spring提供了解決方案 ——IntrospectorCleanupListener:
public class IntrospectorCleanupListener implements ServletContextListener { ? ? @Override ? ? public void contextInitialized(ServletContextEvent event) { ? ? ? ? CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader()); ? ? } ? ? @Override ? ? public void contextDestroyed(ServletContextEvent event) { ? ? ? ? CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader()); ? ? ? ? Introspector.flushCaches(); ? ? } }
IntrospectorCleanupListener是servletContext的監(jiān)聽器,在servletContext銷毀時,會執(zhí)行contextDestroyed方法,調(diào)用Introspector.flushCaches(),防止內(nèi)存泄露。
spring同時說明spring框架沒有使用Introspector的緩存,而是使用Introspector分析bean之后,隨即清理了Introspector緩存,并使用自己的緩存邏輯進(jìn)行緩存,應(yīng)該就是
CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader())
這行代碼實現(xiàn)——未考證,因此spring聲明在只使用spring框架時不需要考慮introspector導(dǎo)致內(nèi)存泄露的問題。
但個人認(rèn)為,如果某個框架在創(chuàng)建自己的線程池時,傳入了新的ThreadGroup對象,那么IntrospectorCleanupListener 可能也無法工作。
Java內(nèi)省Introspector應(yīng)用
IntroSpecor介紹
內(nèi)省(IntroSpector)是Java語言對JavaBean 類屬性、事件的一種缺省處理方法。
例如類A中有屬性name, 那我們可以通過getName,setName 來得到其值或者設(shè)置新的值。
通過getName/setName 來訪問name屬性,這就是默認(rèn)的規(guī)則。
Java中提供了一套API 用來訪問某個屬性的getter/setter方法,通過這些API 可以使你不需要了解這個規(guī)則,這些API存放于包java.beans 中。
Class Diagram
一般的做法是通過類Introspector的getBeanInfo方法獲取某個對象的BeanInfo 信息,然后通過BeanInfo來獲取屬性的描述器(PropertyDescriptor),通過這個屬性描述器就可以獲取某個屬性對應(yīng)的getter/setter方法,然后我們就可以通過反射機制來調(diào)用這些方法。
我們又通常把javabean的實例對象稱之為值對象(Value Object),因為這些bean中通常只有一些信息字段和存儲方法,沒有功能性方法。
一個JavaBean類可以不當(dāng)JavaBean用,而當(dāng)成普通類用。JavaBean實際就是一種規(guī)范,當(dāng)一個類滿足這個規(guī)范,這個類就能被其它特定的類調(diào)用。一個類被當(dāng)作javaBean使用時,JavaBean的屬性是根據(jù)方法名推斷出來的,它根本看不到j(luò)ava類內(nèi)部的成員變量。去掉set前綴,然后取剩余部分,如果剩余部分的第二個字母是小寫的,則把剩余部分的首字母改成小的。
除了反射用到的類需要引入外,內(nèi)省需要引入的類如下所示,它們都屬于java.beans包中的類,自己寫程序的時候也不能忘了引入相應(yīng)的包或者類。
簡單示例1
下面代碼片斷是設(shè)置某個JavaBean類某個屬性的關(guān)鍵代碼:
package com.jasun.test; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.apache.commons.beanutils.BeanUtils; publicclass IntrospectorTest { publi static void main(String[] args) throws IllegalArgumentException, IntrospectionException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { UserInfo userInfo=new UserInfo("zhangsan", "123456"); String propertyName="userName"; Object retVal=getProperty(userInfo, propertyName); System.out.println("retVal="+retVal); //retVal=zhangsan Object value="abc"; setProperty(userInfo, propertyName, value); retVal=getProperty(userInfo, propertyName); System.out.println("retVal="+retVal); //retVal=abc //使用BeanUtils工具包操作JavaBean String userName=BeanUtils.getProperty(userInfo, propertyName); System.out.println("userName="+userName); BeanUtils.setProperty(userInfo, propertyName, "linjiqin"); userName=BeanUtils.getProperty(userInfo, propertyName); System.out.println("userName="+userName); } /** * 設(shè)置屬性 * * @param clazz 對象名 * @param propertyName 屬性名 * @param value 屬性值 */ private static void setProperty(Object clazz, String propertyName, Object value) throws IntrospectionException,IllegalAccessException, InvocationTargetException{ //方法一 /*PropertyDescriptor pd=new PropertyDescriptor(propertyName, clazz.getClass()); Method methodSet=pd.getWriteMethod(); methodSet.invoke(clazz, value);*/ //方法二 BeanInfo beanInfo=Introspector.getBeanInfo(clazz.getClass()); PropertyDescriptor[] pds=beanInfo.getPropertyDescriptors(); for(PropertyDescriptor pd:pds){ if(propertyName.equals(pd.getName())){ Method methodSet=pd.getWriteMethod(); methodSet.invoke(clazz, value); break; } } } /** * 獲取屬性 * * @param clazz 對象名 * @param propertyName 屬性名 * @return * @throws IntrospectionException * @throws InvocationTargetException * @throws IllegalAccessException * @throws IllegalArgumentException */ private static Object getProperty(Object clazz, String propertyName) throws IntrospectionException, IllegalArgumentException, IllegalAccessException, InvocationTargetException{ //方法一 /*PropertyDescriptor pd=new PropertyDescriptor(propertyName, clazz.getClass()); Method methodGet=pd.getReadMethod(); return methodGet.invoke(clazz);*/ //方法二 Object retVal=null; BeanInfo beanInfo=Introspector.getBeanInfo(clazz.getClass()); PropertyDescriptor[] pds=beanInfo.getPropertyDescriptors(); for(PropertyDescriptor pd:pds){ if(propertyName.equals(pd.getName())){ Method methodGet=pd.getReadMethod(); retVal=methodGet.invoke(clazz); break; } } return retVal; } }
UserInfo類
package com.ljq.test; publicclass UserInfo { private String userName; private String pwd; public UserInfo(String userName, String pwd) { super(); this.userName = userName; this.pwd = pwd; } public String getUserName() { return userName; } publicvoid setUserName(String userName) { this.userName = userName; } public String getPwd() { return pwd; } publicvoid setPwd(String pwd) { this.pwd = pwd; } }
簡單示例2::(僅作參考)
package com.siyuan.jdktest; import java.beans.BeanDescriptor; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.MethodDescriptor; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; class Person { private String name; private int age; /** * @return the age */ public int getAge() { return age; } /** * @param age the age to set */ public void setAge(int age) { this.age = age; } /** * @return the name */ public String getName() { return name; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } } public class IntrospectorTest { /** * @param args * @throws IntrospectionException */ public static void main(String[] args) throws IntrospectionException { // TODO Auto-generated method stub BeanInfo beanInfo = Introspector.getBeanInfo(Person.class); System.out.println("BeanDescriptor==========================================="); BeanDescriptor beanDesc = beanInfo.getBeanDescriptor(); Class cls = beanDesc.getBeanClass(); System.out.println(cls.getName()); System.out.println("MethodDescriptor==========================================="); MethodDescriptor[] methodDescs = beanInfo.getMethodDescriptors(); for (int i = 0; i < methodDescs.length; i++) { Method method = methodDescs[i].getMethod(); System.out.println(method.getName()); } System.out.println("PropertyDescriptor==========================================="); PropertyDescriptor[] propDescs = beanInfo.getPropertyDescriptors(); for (int i = 0; i < propDescs.length; i++) { Method methodR = propDescs[i].getReadMethod(); if (methodR != null) { System.out.println(methodR.getName()); } Method methodW = propDescs[i].getWriteMethod(); if (methodW != null) { System.out.println(methodW.getName()); } } } }
運行結(jié)果
BeanDescriptor===========================================
com.siyuan.jdktest.Person
MethodDescriptor===========================================
hashCode
setAge
equals
wait
wait
notify
getClass
toString
getAge
notifyAll
setName
wait
getName
PropertyDescriptor===========================================
getAge
setAge
getClass
getName
setName
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決springboot?druid數(shù)據(jù)庫連接池連接失敗后一直重連問題
這篇文章主要介紹了解決springboot?druid數(shù)據(jù)庫連接池連接失敗后一直重連問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11SpringBoot集成Redisson實現(xiàn)延遲隊列的場景分析
這篇文章主要介紹了SpringBoot集成Redisson實現(xiàn)延遲隊列,本文通過場景分析實例代碼相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04如何用idea編寫并運行第一個spark scala處理程序
詳細(xì)介紹了如何使用IntelliJ IDEA創(chuàng)建Scala項目,包括配置JDK和Scala SDK,添加Maven支持,編輯pom.xml,并創(chuàng)建及運行Scala程序,這為Scala初學(xué)者提供了一個基礎(chǔ)的項目搭建和運行指南2024-09-09SpringCloud Gateway的路由,過濾器和限流解讀
這篇文章主要介紹了SpringCloud Gateway的路由,過濾器和限流解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02spring中的特殊注解@RequiredArgsConstructor詳解
這篇文章主要介紹了spring中的特殊注解@RequiredArgsConstructor,包括注解注入,構(gòu)造器注入及setter注入,結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04