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();
}

實戰案例

需求內容:

  1. 輸出前後資料比對結果

  2. 轉換變數名稱,改為可閱讀內容 Ex. status -> 狀態

  3. 可轉換變數資料結果 Ex. 1:啟用, 0: 停用 -> status:1 -> 狀態:啟用

  4. 排序固定,方便查看比對內容

廢話不多,先看結果。

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"}
    }
}

從結果範例可以看到,這種結果通常會應用到後台使用者操作日誌,以及需要紀錄前後結果對照的功能上,以便釐清前後關係。

FieldInfo
/**
 * @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;
}
BeanUtils
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;
    }

}

以下是針對這五個方法的簡單說明:

  1. getAllFields(Class<?> clazz): 這個方法用於獲取一個類及其超類中的所有字段,並根據特定的條件(通過 FieldInfo 注解)進行篩選。如果類不是 CGLIB 代理類,則優先從 fieldsMap 中獲取,否則調用 _getAllFields 方法獲取並存入緩存中。

  2. _getAllFields(Class<?> clazz): 這個私有方法實際上執行字段的提取工作。它首先獲取指定類的所有宣告字段,然後通過 excludeField 方法進行篩選,接著繼續處理超類的字段,直到所有字段被提取並返回。

  3. excludeField(List<Field> fieldList): 這個方法是 _getAllFields 中使用的輔助方法,它通過過濾字段列表,只保留標有 FieldInfo 注解且允許包含(通過 include() 方法)的字段。

  4. extract(Object obj): 這個方法從給定的對象中提取字段信息並創建一個映射,其中包含字段名稱和值。它使用 BeanUtils.getAllFields 獲取所有字段,然後使用反射獲取字段的值。如果該字段允許空值,或者字段值不為空,則將字段信息添加到映射中。

  5. compare(Object from, Object to, String... ignoreFields): 這個方法用於比較兩個對象的字段變更。它使用 extract 方法提取兩個對象的字段信息,然後對比這些信息,找到不同的字段。忽略指定的字段(如果有的話),最終返回一個映射,其中包含變更的字段名稱和相關信息。

  6. transExternalName(ConvertExternalNameWrapper convertWrapper): 這個方法接受一個 ConvertExternalNameWrapper 對象,該對象包含比較結果的映射,將這些比較結果轉換為外部名稱的形式,並返回一個更新後的 ConvertExternalNameWrapper 對象。在這個方法中,首先創建了兩個空的映射 fromMaptoMap 用於存儲轉換後的數據。

這五個方法一起提供了一個工具集,用於處理對象的字段操作、篩選和比較。它們基於反射和 Java 的反射 API,讓你可以更便捷地操作和分析對象的字段數據。

Last updated