在 Java 開發中,JavaBean (POJO) 與 Map<String, Object> 互轉是極高頻需求:表單綁定、參數傳遞、快取序列化、資料庫結果映射、API 回應構建等。本文提供基於 **Java 標準 Introspector API** 的生產級工具類,支援巢狀屬性、泛型集合、空值處理、效能優化,並分析替代方案優劣。
java.beans.Introspector 是 JDK 內建的 JavaBean 內省機制,能自動分析類別的 **屬性描述符** (PropertyDescriptor):
getXxx() / isXxx() (boolean)setXxx()getUserName() → userName)**優點**:零依賴、標準規範、支援繼承鏈、自動快取 BeanInfo。
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
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(ClasssourceType, ClasstargetType, 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);
isActive() → 屬性名 active;getActive() → active。混用可能導致重複屬性。List 自動轉 List (需自行遞迴處理)。Introspector.getBeanInfo(clazz, Object.class) 會包含父類屬性 (排除 Object 類)。基於 Introspector 的 Map-Bean 互轉工具類是 Java 開發者的「瑞士刀」:**零依賴、標準化、效能優異、易擴展**。掌握其核心 API 與快取機制,配合類型轉換器擴展,即可優雅解決 90% 的物件映射場景。若專案已引入 Spring/Jackson/MapStruct,優先使用生態原生方案以獲得更豐富功能與類型安全。