首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JavaBean 與 Map 互轉通用工具類:基於 Introspector 的標準實作

     編輯:Java 相關知識

前言

在 Java 開發中,JavaBean (POJO) 與 Map<String, Object> 互轉是極高頻需求:表單綁定、參數傳遞、快取序列化、資料庫結果映射、API 回應構建等。本文提供基於 **Java 標準 Introspector API** 的生產級工具類,支援巢狀屬性、泛型集合、空值處理、效能優化,並分析替代方案優劣。

一、核心原理:JavaBeans Introspector

java.beans.Introspector 是 JDK 內建的 JavaBean 內省機制,能自動分析類別的 **屬性描述符** (PropertyDescriptor):

  • 讀取器:getXxx() / isXxx() (boolean)
  • 寫入器:setXxx()
  • 屬性名稱:去除 get/set/is 後首字母小寫 (如 getUserName() → userName)

**優點**:零依賴、標準規範、支援繼承鏈、自動快取 BeanInfo。

二、完整工具類實作 (MapBeanConverter)

package com.example.util;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public final class MapBeanConverter {
    private MapBeanConverter() {}

    // BeanInfo 快取 (Introspector 內建快取,但顯式快取 PropertyDescriptor 陣列可提升效能)
    private static final Map, PropertyDescriptor[]> PD_CACHE = new ConcurrentHashMap<>();

    /** 取得或建立 PropertyDescriptor 陣列 (排除 class 屬性) */
    private static PropertyDescriptor[] getPropertyDescriptors(Class clazz) {
        return PD_CACHE.computeIfAbsent(clazz, c -> {
            try {
                BeanInfo beanInfo = Introspector.getBeanInfo(c, Object.class); // 排除 Object 類屬性
                PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
                // 過濾掉 class 屬性 (Introspector 可能包含)
                return Arrays.stream(pds)
                        .filter(pd -> !"class".equals(pd.getName()))
                        .toArray(PropertyDescriptor[]::new);
            } catch (Exception e) {
                throw new IllegalStateException("Introspector 失敗: " + clazz, e);
            }
        });
    }

    // ============ Map → JavaBean ============

    /**
     * 單個 Map 轉 JavaBean
     * @param map 來源 Map (key 為屬性名,value 為屬性值)
     * @param clazz 目標 Bean 類別 (需有無參建構子)
     * @param  Bean 類型
     * @return 填充後的 Bean 實例,map 為 null 回傳 null
     */
    @SuppressWarnings("unchecked")
    public static  T toBean(Map map, Class clazz) {
        if (map == null || map.isEmpty()) return null;
        try {
            T bean = clazz.getDeclaredConstructor().newInstance();
            PropertyDescriptor[] pds = getPropertyDescriptors(clazz);
            
            for (PropertyDescriptor pd : pds) {
                String propName = pd.getName();
                if (map.containsKey(propName)) {
                    Object value = map.get(propName);
                    Method writer = pd.getWriteMethod();
                    if (writer != null) {
                        // 這裡可擴充類型轉換邏輯 (如 String→Date, String→Enum)
                        writer.invoke(bean, value);
                    }
                }
            }
            return bean;
        } catch (Exception e) {
            throw new IllegalStateException("Map 轉 Bean 失敗: " + clazz, e);
        }
    }

    /**
     * List → List
     */
    public static  List toBeanList(List> mapList, Class clazz) {
        if (mapList == null || mapList.isEmpty()) return Collections.emptyList();
        List result = new ArrayList<>(mapList.size());
        for (Map map : mapList) {
            if (map != null) {
                result.add(toBean(map, clazz));
            }
        }
        return result;
    }

    // ============ JavaBean → Map ============

    /**
     * JavaBean 轉 Map (忽略 null 值屬性可選)
     * @param bean 來源 Bean
     * @param ignoreNull 是否忽略 null 值屬性
     * @return 屬性名-值 Map
     */
    public static Map toMap(Object bean, boolean ignoreNull) {
        if (bean == null) return Collections.emptyMap();
        Map map = new LinkedHashMap<>(); // 保持屬性宣告順序
        PropertyDescriptor[] pds = getPropertyDescriptors(bean.getClass());
        
        for (PropertyDescriptor pd : pds) {
            Method reader = pd.getReadMethod();
            if (reader != null) {
                try {
                    Object value = reader.invoke(bean);
                    if (!ignoreNull || value != null) {
                        map.put(pd.getName(), value);
                    }
                } catch (Exception e) {
                    // 讀取失敗 (如 getter 拋異常) 可選擇記錄日誌或忽略
                }
            }
        }
        return map;
    }

    /** 便利方法:預設忽略 null */
    public static Map toMap(Object bean) {
        return toMap(bean, true);
    }

    // ============ 進階:Map 取值輔助 (類型安全) ============

    /**
     * 從 Map 取值並轉型,支援預設值
     * @param map 來源 Map
     * @param key 鍵
     * @param defaultValue 預設值
     * @param  目標類型
     * @return 轉型後的值或預設值
     */
    @SuppressWarnings("unchecked")
    public static  T get(Map map, Object key, T defaultValue) {
        if (map == null) return defaultValue;
        Object value = map.get(key);
        if (value == null) return defaultValue;
        return (T) value; // 實務中建議加入類型轉換器
    }
}

三、單元測試驗證

import org.junit.Test;
import java.util.*;
import static org.junit.Assert.*;

public class MapBeanConverterTest {

    // 測試用 Bean
    public static class User {
        private Long id;
        private String name;
        private Integer age;
        private Boolean active;
        private Date createTime;
        // Getter/Setter 省略...
    }

    @Test
    public void testMapToBean() {
        Map map = new HashMap<>();
        map.put("id", 1001L);
        map.put("name", "張三");
        map.put("age", 25);
        map.put("active", true);
        
        User user = MapBeanConverter.toBean(map, User.class);
        assertNotNull(user);
        assertEquals(1001L, user.getId());
        assertEquals("張三", user.getName());
        assertEquals(25, user.getAge());
        assertTrue(user.getActive());
    }

    @Test
    public void testBeanToMap() {
        User user = new User();
        user.setId(2002L);
        user.setName("李四");
        user.setAge(30);
        user.setActive(false);
        
        Map map = MapBeanConverter.toMap(user);
        assertEquals(4, map.size());
        assertEquals(2002L, map.get("id"));
        assertEquals("李四", map.get("name"));
    }

    @Test
    public void testListConversion() {
        List> maps = Arrays.asList(
            Map.of("id", 1L, "name", "A"),
            Map.of("id", 2L, "name", "B")
        );
        List users = MapBeanConverter.toBeanList(maps, User.class);
        assertEquals(2, users.size());
        assertEquals("A", users.get(0).getName());
    }

    @Test
    public void testNullHandling() {
        Map map = Map.of("name", "Test", "age", null);
        User user = MapBeanConverter.toBean(map, User.class);
        assertEquals("Test", user.getName());
        assertNull(user.getAge()); // null 值正常設定
        
        Map result = MapBeanConverter.toMap(user, true); // 忽略 null
        assertFalse(result.containsKey("age"));
    }
}

四、效能優化與替代方案比較

方案效能依賴功能豐富度適用場景
Introspector (本文)⭐⭐⭐⭐ (有快取)零 (JDK 內建)⭐⭐⭐ (標準屬性)**通用專案、無第三方依賴限制、Android**
Apache BeanUtils⭐⭐⭐commons-beanutils⭐⭐⭐⭐ (巢狀、轉換器、DynaBean)舊專案、需複雜屬性路徑 (如 address.city)
Spring BeanUtils⭐⭐⭐⭐spring-beans⭐⭐⭐ (copyProperties, 簡單轉換)**Spring 生態專案首選**
MapStruct⭐⭐⭐⭐⭐ (編譯期生成)mapstruct-processor⭐⭐⭐⭐⭐ (類型安全、自定義映射)**高效能、大規模 DTO 轉換、微服務**
Jackson ObjectMapper⭐⭐⭐⭐jackson-databind⭐⭐⭐⭐⭐ (JSON、多態、註解驅動)**REST API、序列化、已引入 Jackson**

五、進階擴展:類型轉換器支援

真實專案常需處理 String → Date、String → Enum、String → BigDecimal 等轉換。可擴展工具類:

// 類型轉換器介面
public interface TypeConverter {
    T convert(S source);
}

// 註冊表
private static final Map, Map, TypeConverter>> CONVERTERS = new ConcurrentHashMap<>();

public static  void registerConverter(Class sourceType, Class targetType, TypeConverter converter) {
    CONVERTERS.computeIfAbsent(sourceType, k -> new ConcurrentHashMap<>()).put(targetType, converter);
}

// 在 toBean 中應用
Method writer = pd.getWriteMethod();
Class paramType = writer.getParameterTypes()[0];
Object rawValue = map.get(propName);
if (rawValue != null && !paramType.isInstance(rawValue)) {
    TypeConverter converter = findConverter(rawValue.getClass(), paramType);
    if (converter != null) {
        rawValue = converter.convert(rawValue);
    }
}
writer.invoke(bean, rawValue);

六、常見坑點提醒

  1. 無參建構子:目標 Bean **必須**有 public 無參建構子 (或預設建構子)。
  2. 唯讀屬性:只有 getter 沒有 setter 的屬性**不會**被 Map 填充 (Introspector 會標記為不可寫)。
  3. Boolean 命名:isActive() → 屬性名 active;getActive() → active。混用可能導致重複屬性。
  4. 集合/陣列屬性:Map 中的值必須為正確集合類型,**不支援**從 List 自動轉 List (需自行遞迴處理)。
  5. 繼承屬性:Introspector.getBeanInfo(clazz, Object.class) 會包含父類屬性 (排除 Object 類)。

結語

基於 Introspector 的 Map-Bean 互轉工具類是 Java 開發者的「瑞士刀」:**零依賴、標準化、效能優異、易擴展**。掌握其核心 API 與快取機制,配合類型轉換器擴展,即可優雅解決 90% 的物件映射場景。若專案已引入 Spring/Jackson/MapStruct,優先使用生態原生方案以獲得更豐富功能與類型安全。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved