前言
最新工作中,遇到了通過反射調用get/set方法的地方,雖然反射的性能不是很好,但是相比較於硬編碼的不易擴展,getDeclareFields可以拿到所有的成員變量,后續添加或刪除成員變量時,不用修改代碼,且應用次數只在修改數據時使用,故犧牲一些性能提高擴展性
傳統的方式
見過很多人通過反射調用get/set方法都是通過獲取屬性的name,然后通過字符串截取將首字母大寫,再拼上get/set來做
String fieldName = field.getName();
String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
還有稍微好一點的同學,通過fieldName轉成字符數組,首個字符-32來避免字符串截取的
String fieldName = field.getName(); char[] chars = fieldName.toCharArray(); chars[0] = (char)(chars[0] - 32); String getMethodName = "get" + new String(chars);
誠然,我覺得兩種方式都可以,但是不知道有沒有遇到過,生成的get/set方法並不是已get/set開頭的,而是以is開頭的,比如boolean類型的成員變量。這個時候我們就需要去判斷屬性的類型,然后用不同的前綴來拼接get/set方法名。其實,在jdk中已經包含了這樣的工具類
Introspector和PropertyDescriptor
關於這兩個類的詳細介紹,我這里就不說了,簡單的理解就是對象信息的描述,里面提供了一些API方便我們拿到對象的信息
BeanInfo beanInfo; try { beanInfo = Introspector.getBeanInfo(template.getClass()); } catch (IntrospectionException e) { log.info("xxxxxxxxxxxxxxxx", e); return null; } List<PropertyDescriptor> descriptors = Arrays.stream(beanInfo.getPropertyDescriptors()).filter(p -> { String name = p.getName(); //過濾掉不需要修改的屬性 return !"class".equals(name) && !"id".equals(name); }).collect(Collectors.toList()); for (PropertyDescriptor descriptor : descriptors) { //descriptor.getWriteMethod()方法對應set方法 Method readMethod = descriptor.getReadMethod(); System.out.println(descriptor.getName()); try { Object o = readMethod.invoke(template); System.out.println(o); } catch (IllegalAccessException | InvocationTargetException e) { log.info("xxxxxxxxxxxxxxxx", e); return null; } }
PropertyDescriptor類提供了getReadMethod和getWriteMethod,其實就是對於get/set方法,至於方法名稱不需要我們來關於,這樣就可以避免方法名拼錯的情況了。
另外PropertyDescriptor除了可以通過Introspector獲取,也可以自己new來創建,其構造方法還是比較全的
通常傳遞一個屬性的名稱和類對象class就可以了
List<Field> fields = Arrays.stream(template.getClass().getDeclaredFields()).filter(f -> { String name = f.getName(); //過濾掉不需要修改的屬性 return !"id".equals(name) && !"serialVersionUID".equals(name); }).collect(Collectors.toList()); for (Field field : fields) { try { PropertyDescriptor descriptor = new PropertyDescriptor(field.getName(), template.getClass()); Method readMethod = descriptor.getReadMethod(); Object o = readMethod.invoke(template); System.out.println(o); } catch (IntrospectionException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } }
通過上面兩種不同的實現方式可以看到,Introspector會額外有一個class屬性,但是類似serialVersionUID不會算在內;而自定義PropertyDescriptor需要通過反射拿到所有的屬性,雖然不會有class屬性,但是serialVersionUID會算在內,使用的時候需要注意一下。
如果你以為這就是Introspector的全部功能,那就大錯特錯了。Introspector不同於普通的反射,反射一次,一段時間內可重復使用,為什么不是永久呢,看下源碼
/** * Introspect on a Java Bean and learn about all its properties, exposed * methods, and events. * <p> * If the BeanInfo class for a Java Bean has been previously Introspected * then the BeanInfo class is retrieved from the BeanInfo cache. * * @param beanClass The bean class to be analyzed. * @return A BeanInfo object describing the target bean. * @exception IntrospectionException if an exception occurs during * introspection. * @see #flushCaches * @see #flushFromCaches */ 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; }
注意中間加粗標紅的代碼,這里除了同步之外,還做了一個本地的緩存
BeanInfo getBeanInfo(Class<?> type) { return (this.beanInfoCache != null) ? this.beanInfoCache.get(type) : null; }
這個beanInfoCache 其實是一個WeakHashMap,每次gc被回收,所以上面說一段時間內可以重復使用而不是永久,也是為了避免OOM吧
總結
大概先說這么多吧,雖然算不上什么高級技術,但是能將工作中遇到的小問題解決也是成長啊!