Reflect 應用
從 Class 獲取資訊
Class 物件表示所載入的類別,取得Class物件之後,你就可以取得與類別相關聯的資訊,像是套件(package)(別忘了package也是類別名稱的一部 份)、建構方法、方法成員、資料成員等的訊息,而每一個訊息,也會有相應的類別型態,例如套件的對應型態是java.lang.Package
,建構方法的對應型態是java.lang.reflect.Constructor
,方法成員的對應型態是java.lang.reflect.Method
,資料成員的對應型態是java.lang.reflect.Field
等。
來看個簡單的示範,以下可以讓您取得所指定類別上的套件名稱:
try {
Class c = Class.forName(args[0]);
Package p = c.getPackage();
System.out.println(p.getName());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("沒有指定類別");
} catch (ClassNotFoundException e) {
System.out.println("找不到指定類別");
}
你可以分別取回Field、Constructor、Method
等物件,分別代表資料成員、建構方法與方法成員,以下範例簡單地實作了取得類別基本資訊的程式:
try {
Class c = Class.forName(args[0]);
// 取得套件代表物件
Package p = c.getPackage();
System.out.printf("package %s;%n", p.getName());
// 取得型態修飾,像是class、interface
int m = c.getModifiers();
System.out.print(Modifier.toString(m) + " ");
// 如果是介面
if (Modifier.isInterface(m)) {
System.out.print("interface ");
} else {
System.out.print("class ");
}
System.out.println(c.getName() + " {");
// 取得宣告的資料成員代表物件
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
// 顯示權限修飾,像是public、protected、private
System.out.print("\t" + Modifier.toString(field.getModifiers()));
// 顯示型態名稱
System.out.print(" " + field.getType().getName() + " ");
// 顯示資料成員名稱
System.out.println(field.getName() + ";");
}
// 取得宣告的建構方法代表物件
Constructor[] constructors = c.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// 顯示權限修飾,像是public、protected、private
System.out.print("\t" + Modifier.toString(constructor.getModifiers()));
// 顯示建構方法名稱
System.out.println(" " + constructor.getName() + "();");
}
// 取得宣告的方法成員代表物件
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {
// 顯示權限修飾,像是public、protected、private
System.out.print("\t" + Modifier.toString(method.getModifiers()));
// 顯示返回值型態名稱
System.out.print(" " + method.getReturnType().getName() + " ");
// 顯示方法名稱
System.out.println(method.getName() + "();");
}
System.out.println("}");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("沒有指定類別");
} catch (ClassNotFoundException e) {
System.out.println("找不到指定類別");
}
亦可呼叫物件內方法
看看以下範例以最簡單的Get/Set
方法
public static void reflectTest() {
try {
Class c = new AccountingReport().getClass();
Object stockObj = c.getDeclaredConstructor().newInstance();
// 呼叫Set方法
Method[] methods = c.getMethods();
for (Method method : methods) {
if ("setCode".equals(method.getName()))
method.invoke(stockObj, "1100");
else if("setName".equals(method.getName()))
method.invoke(stockObj, "現金及約當現金");
}
System.out.println(stockObj);
// 呼叫Set方法
for (Method method : methods) {
if ("getCode".equals(method.getName()))
System.out.println("get code value:" + method.invoke(stockObj));
else if("getName".equals(method.getName()))
System.out.println("get name value:" + method.invoke(stockObj));
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("沒有指定類別");
} catch (Exception e) {
e.printStackTrace();
System.out.println("找不到指定類別");
}
}
進階應用 - auto get/set value
/**
* NamingUtils 此物件為將變數名稱駝峰命名
* 將含底線字串轉換為首字母小寫駝峰命名
* Ex. set_code => setCode
*/
public static <T> T autoSetValue(T target, String fieldName, Object data){
try {
Class c = target.getClass();
Method[] methods = c.getMethods();
String setMethodName = NamingUtils.camelize("set_" + fieldName);
for(Method method : methods){
if(setMethodName.equals(method.getName())){
method.invoke(target,data);
}
}
return target;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("沒有指定類別");
} catch (Exception e) {
System.out.println("找不到指定類別");
}
return target;
}
public static <T> Optional<Object> autoGetValue(T source, String fieldName) {
try {
Class c = source.getClass();
Method[] methods = c.getMethods();
String getMethodName = NamingUtils.camelize("get_" + fieldName);
for (Method method : methods) {
if (getMethodName.equals(method.getName()))
return Optional.of(method.invoke(source));
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("沒有指定類別");
} catch (Exception e) {
System.out.println("找不到指定類別");
}
return Optional.empty();
}
實戰案例
需求內容:
輸出前後資料比對結果
轉換變數名稱,改為可閱讀內容 Ex. status -> 狀態
可轉換變數資料結果 Ex. 1:啟用, 0: 停用 -> status:1 -> 狀態:啟用
排序固定,方便查看比對內容
廢話不多,先看結果。
public class BeanUtilTest_ {
public static void main(String[] args) {
DemoBean bean = new DemoBean().setDate(new Date(System.currentTimeMillis() - 60000l))
.setType("A")
.setInteger(158)
.setBigDecimal(BigDecimal.valueOf(177l))
.setStatus(CommonOnOffStatus.ON);
DemoBean bean2 = new DemoBean().setDate(new Date())
.setType("B")
.setInteger(200)
.setBigDecimal(BigDecimal.valueOf(307l))
.setStatus(CommonOnOffStatus.OFF);
Map<String, FieldChange> compareResultMap = BeanUtils.compare(bean, bean2);
Map<String, Function> convertFunctionMap = new HashMap<>();
convertFunctionMap.put("status", (Function<CommonOnOffStatus, String>) o -> Objects.nonNull(o) ? o.getExternalName() : "");
convertFunctionMap.put("type", (Function<String, String>) o -> StringUtils.isNotBlank(o) && o.equals("A") ? "A類型" : "B類型");
ConvertExternalNameWrapper wrapper = BeanUtils.transExternalName(new ConvertExternalNameWrapper(compareResultMap, convertFunctionMap));
System.out.println(new JSONObject(wrapper.getFromMap()));
System.out.println(new JSONObject(wrapper.getToMap()));
// output
// {"日期":"2023-08-28 11:29:32","整數":"158","用戶狀態":"啟用","類型":"A類型","十進制":"177.000000"}
// {"日期":"2023-08-28 11:30:32","整數":"200","用戶狀態":"停用","類型":"B類型","十進制":"307.000000"}
}
}
{
"日期": "2023-08-28 11:29:32",
"整數": "158",
"用戶狀態": "啟用",
"類型": "A類型",
"十進制": "177.000000"
}
{
"日期": "2023-08-28 11:30:32",
"整數": "200",
"用戶狀態": "停用",
"類型": "B類型",
"十進制": "307.000000"
}
從結果範例可以看到,這種結果通常會應用到後台使用者操作日誌,以及需要紀錄前後結果對照的功能上,以便釐清前後關係。
/**
* @author caster.hsu
* @Since 2023/8/11
*/
@Target(value = ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldInfo {
String desc() default "";
boolean allowNullValue() default true;
boolean include() default true;
}
package com.caster.test.bean;
import com.caster.test.bean.anno.FieldInfo;
import com.caster.test.bean.field.FieldChange;
import com.caster.test.bean.field.ConvertExternalNameWrapper;
import com.caster.test.bean.field.FieldWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.beanutils.PropertyUtils;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
@Slf4j
public class BeanUtils {
private static final Map<Class, List<Field>> fieldsMap = new HashMap<Class, List<Field>>();
/**
* 檢索類別字段及排除特定注解,並暫存暫存class提高效能。
*
* @param clazz
* @return
*/
public static List<Field> getAllFields(Class<?> clazz) {
if (!ClassUtils.isCglibProxyClass(clazz)) {
List<Field> result = fieldsMap.get(clazz);
if (result == null) {
result = _getAllFields(clazz);
fieldsMap.put(clazz, result);
}
return result;
} else {
return _getAllFields(clazz);
}
}
private static List<Field> _getAllFields(Class<?> clazz) {
List<Field> fieldList = new ArrayList<Field>();
Collections.addAll(fieldList, clazz.getDeclaredFields());
excludeField(fieldList);
Class<?> c = clazz.getSuperclass();
if (c != null) {
fieldList.addAll(getAllFields(c));
}
return fieldList;
}
private static List<Field> excludeField(List<Field> fieldList) {
return fieldList.stream()
.filter(o -> o.isAnnotationPresent(FieldInfo.class))
.filter(o -> o.getAnnotation(FieldInfo.class).include())
.collect(Collectors.toList());
}
/**
* Get field in class and its super class
*
* @param clazz
* @param fieldName
* @return
*/
public static Field getField(Class<?> clazz, String fieldName) {
for (Field field : clazz.getDeclaredFields()) {
if (field.getName().equals(fieldName)) {
return field;
}
}
Class<?> c = clazz.getSuperclass();
if (c != null) {
return getField(c, fieldName);
}
return null;
}
public static Map<String, FieldWrapper> extract(Object obj) {
Map<String, FieldWrapper> result = new HashMap<String, FieldWrapper>();
for (Field field : BeanUtils.getAllFields(obj.getClass())) {
try {
FieldInfo fieldAnno = field.getAnnotation(FieldInfo.class);
Object value = PropertyUtils.getProperty(obj, field.getName()); // here has large log out put
if ((Objects.nonNull(fieldAnno) && fieldAnno.allowNullValue()) || Objects.nonNull(value))
result.put(field.getName(), FieldWrapper.create(field, value));
} catch (Exception e) {
//log.error("", e);'
e.printStackTrace();
}
}
return result;
}
public static Map<String, FieldChange> compare(Object from, Object to, String... ignoreFields) {
Map<String, FieldChange> result = new HashMap<String, FieldChange>();
Map<String, FieldWrapper> valueList1 = extract(from);
Map<String, FieldWrapper> valueList2 = extract(to);
Set<String> ignoreFieldSet = new HashSet<String>();
if (ignoreFields != null)
Collections.addAll(ignoreFieldSet, ignoreFields);
for (String name : valueList1.keySet()) {
if (ignoreFieldSet.contains(name))
continue;
FieldWrapper fv1 = valueList1.get(name);
FieldWrapper fv2 = valueList2.get(name);
if (fv1 != null && fv2 != null && !fv1.equals(fv2)) {
result.put(name, new FieldChange(name, fv1, fv2));
}
}
return result;
}
public static ConvertExternalNameWrapper transExternalName(ConvertExternalNameWrapper convertWrapper) {
Map<String, Object> fromMap = new HashMap<>();
Map<String, Object> toMap = new HashMap<>();
for (FieldChange f : convertWrapper.getCompareResultMap().values()) {
fromMap.putAll(f.getFrom().toExternalString((o) -> {
FieldWrapper wrapper = (FieldWrapper) o;
String value = wrapper.stringValue();
if (convertWrapper.getConvertFunctionMap().containsKey(wrapper.getVariableName()))
return convertWrapper.getConvertFunctionMap().get(wrapper.getVariableName()).apply(wrapper.getValue());
return value;
}));
toMap.putAll(f.getTo().toExternalString((o) -> {
FieldWrapper wrapper = (FieldWrapper) o;
String value = wrapper.stringValue();
if (convertWrapper.getConvertFunctionMap().containsKey(wrapper.getVariableName()))
return convertWrapper.getConvertFunctionMap().get(wrapper.getVariableName()).apply(wrapper.getValue());
return value;
}));
}
convertWrapper.setFromMap(fromMap);
convertWrapper.setToMap(toMap);
return convertWrapper;
}
}
以下是針對這五個方法的簡單說明:
getAllFields(Class<?> clazz)
: 這個方法用於獲取一個類及其超類中的所有字段,並根據特定的條件(通過FieldInfo
注解)進行篩選。如果類不是 CGLIB 代理類,則優先從fieldsMap
中獲取,否則調用_getAllFields
方法獲取並存入緩存中。_getAllFields(Class<?> clazz)
: 這個私有方法實際上執行字段的提取工作。它首先獲取指定類的所有宣告字段,然後通過excludeField
方法進行篩選,接著繼續處理超類的字段,直到所有字段被提取並返回。excludeField(List<Field> fieldList)
: 這個方法是_getAllFields
中使用的輔助方法,它通過過濾字段列表,只保留標有FieldInfo
注解且允許包含(通過include()
方法)的字段。extract(Object obj)
: 這個方法從給定的對象中提取字段信息並創建一個映射,其中包含字段名稱和值。它使用BeanUtils.getAllFields
獲取所有字段,然後使用反射獲取字段的值。如果該字段允許空值,或者字段值不為空,則將字段信息添加到映射中。compare(Object from, Object to, String... ignoreFields)
: 這個方法用於比較兩個對象的字段變更。它使用extract
方法提取兩個對象的字段信息,然後對比這些信息,找到不同的字段。忽略指定的字段(如果有的話),最終返回一個映射,其中包含變更的字段名稱和相關信息。transExternalName(ConvertExternalNameWrapper convertWrapper)
: 這個方法接受一個ConvertExternalNameWrapper
對象,該對象包含比較結果的映射,將這些比較結果轉換為外部名稱的形式,並返回一個更新後的ConvertExternalNameWrapper
對象。在這個方法中,首先創建了兩個空的映射fromMap
和toMap
用於存儲轉換後的數據。
這五個方法一起提供了一個工具集,用於處理對象的字段操作、篩選和比較。它們基於反射和 Java 的反射 API,讓你可以更便捷地操作和分析對象的字段數據。
Last updated