⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7118073840999071751 「明明如月学长」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

一、背景

有些业务场景下需要将 Java Bean 转成 Map 再使用。本以为很简单场景,但是坑很多。

二、那些坑

2.0 测试对象

import lombok.Data;

import java.util.Date;

@Data
public class MockObject extends MockParent{

private Integer aInteger;

private Long aLong;

private Double aDouble;

private Date aDate;
}

父类

import lombok.Data;

@Data
public class MockParent {
private Long parent;
}

2.1 JSON 反序列化了类型丢失

2.1.1 问题复现

将 Java Bean 转 Map 最常见的手段就是使用 JSON 框架,如 fastjson 、 gson、jackson 等。但使用 JSON 将 Java Bean 转 Map 会导致部分数据类型丢失。如使用 fastjson ,当属性为 Long 类型但数字小于 Integer 最大值时,反序列成 Map 之后,将变为 Integer 类型。

maven 依赖:

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.8</version>
</dependency>

示例代码:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;

import java.util.Date;
import java.util.Map;

public class JsonDemo {

public static void main(String[] args) {
MockObject mockObject = new MockObject();
mockObject.setAInteger(1);
mockObject.setALong(2L);
mockObject.setADate(new Date());
mockObject.setADouble(3.4D);
mockObject.setParent(3L);

String json = JSON.toJSONString(mockObject);

Map<String,Object> map = JSON.parseObject(json, new TypeReference<Map<String,Object>>(){});

System.out.println(map);
}
}

结果打印:

{"parent":3,"ADouble":3.4,"ALong":2,"AInteger":1,"ADate":1657299916477}

调试截图:

通过 Java Visualizer 插件进行可视化查看:

2.2.2 问题描述

存在两个问题

(1) 通过 fastjson 将 Java Bean 转为 Map ,类型会发生转变。如 Long 变成 Integer ,Date 变成 Long, Double 变成 Decimal 类型等。

(2)在某些场景下,Map 的 key 并非和属性名完全对应,像是通过 get set 方法“推断”出来的属性名。

2.2 BeanMap 转换属性名错误

2.2.1 commons-beanutils 的 BeanMap

maven 版本:

<!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>

代码示例:

import org.apache.commons.beanutils.BeanMap;
import third.fastjson.MockObject;

import java.util.Date;

public class BeanUtilsDemo {
public static void main(String[] args) {
MockObject mockObject = new MockObject();
mockObject.setAInteger(1);
mockObject.setALong(2L);
mockObject.setADate(new Date());
mockObject.setADouble(3.4D);
mockObject.setParent(3L);

BeanMap beanMap = new BeanMap(mockObject);
System.out.println(beanMap);
}
}

调试截图:

存在和 cglib 一样的问题,虽然类型没问题但是属性名还是不对。

原因分析:

/**
* Constructs a new <code>BeanMap</code> that operates on the
* specified bean. If the given bean is <code>null</code>, then
* this map will be empty.
*
* @param bean the bean for this map to operate on
*/
public BeanMap(final Object bean) {
this.bean = bean;
initialise();
}

关键代码:

private void initialise() {
if(getBean() == null) {
return;
}

final Class<? extends Object> beanClass = getBean().getClass();
try {
//BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
final BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
if ( propertyDescriptors != null ) {
for (final PropertyDescriptor propertyDescriptor : propertyDescriptors) {
if ( propertyDescriptor != null ) {
final String name = propertyDescriptor.getName();
final Method readMethod = propertyDescriptor.getReadMethod();
final Method writeMethod = propertyDescriptor.getWriteMethod();
final Class<? extends Object> aType = propertyDescriptor.getPropertyType();

if ( readMethod != null ) {
readMethods.put( name, readMethod );
}
if ( writeMethod != null ) {
writeMethods.put( name, writeMethod );
}
types.put( name, aType );
}
}
}
}
catch ( final IntrospectionException e ) {
logWarn( e );
}
}

调试一下就会发现,问题出在 BeanInfo 里面 PropertyDescriptor 的 name 不正确。

经过分析会发现 java.beans.Introspector#getTargetPropertyInfo 方法是字段解析的关键

对于无参的以 get 开头的方法名从 index =3 处截取,如 getALong 截取后为 ALong, 如 getADouble 截取后为 ADouble。

然后去构造 PropertyDescriptor:

/**
* Creates <code>PropertyDescriptor</code> for the specified bean
* with the specified name and methods to read/write the property value.
*
* @param bean the type of the target bean
* @param base the base name of the property (the rest of the method name)
* @param read the method used for reading the property value
* @param write the method used for writing the property value
* @exception IntrospectionException if an exception occurs during introspection
*
* @since 1.7
*/
PropertyDescriptor(Class<?> bean, String base, Method read, Method write) throws IntrospectionException {
if (bean == null) {
throw new IntrospectionException("Target Bean class is null");
}
setClass0(bean);
setName(Introspector.decapitalize(base));
setReadMethod(read);
setWriteMethod(write);
this.baseName = base;
}

底层使用 java.beans.Introspector#decapitalize 进行解析:

/**
* Utility method to take a string and convert it to normal Java variable
* name capitalization. This normally means converting the first
* character from upper case to lower case, but in the (unusual) special
* case when there is more than one character and both the first and
* second characters are upper case, we leave it alone.
* <p>
* Thus "FooBah" becomes "fooBah" and "X" becomes "x", but "URL" stays
* as "URL".
*
* @param name The string to be decapitalized.
* @return The decapitalized version of the string.
*/
public static String decapitalize(String name) {
if (name == null || name.length() == 0) {
return name;
}
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
Character.isUpperCase(name.charAt(0))){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}

从代码中我们可以看出 (1) 当 name 的长度 > 1,且第一个字符和第二个字符都大写时,直接返回参数作为PropertyDescriptor name。(2) 否则将 name 转为首字母小写

这种处理本意是为了不让属性为类似 URL 这种缩略词转为 uRL ,结果“误伤”了我们这种场景。

2.2.2 使用 cglib 的 BeanMap

cglib 依赖

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.2.12</version>
</dependency>

代码示例:

import net.sf.cglib.beans.BeanMap;
import third.fastjson.MockObject;

import java.util.Date;

public class BeanMapDemo {

public static void main(String[] args) {

MockObject mockObject = new MockObject();
mockObject.setAInteger(1);
mockObject.setALong(2L);
mockObject.setADate(new Date());
mockObject.setADouble(3.4D);
mockObject.setParent(3L);

BeanMap beanMapp = BeanMap.create(mockObject);

System.out.println(beanMapp);
}
}

结果展示:

我们发现类型对了,但是属性名依然不对。

关键代码:net.sf.cglib.core.ReflectUtils#getBeanGetters 底层也会用到 java.beans.Introspector#decapitalize 所以属性名存在一样的问题就不足为奇了。

三、解决办法

3.1 解决方案

解决方案有很多,本文提供一个基于 dubbo的解决方案。

maven 依赖:

<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>3.0.9</version>
</dependency>

示例代码:

import org.apache.dubbo.common.utils.PojoUtils;
import third.fastjson.MockObject;

import java.util.Date;

public class DubboPojoDemo {
public static void main(String[] args) {
MockObject mockObject = new MockObject();
mockObject.setAInteger(1);
mockObject.setALong(2L);
mockObject.setADate(new Date());
mockObject.setADouble(3.4D);
mockObject.setParent(3L);

Object generalize = PojoUtils.generalize(mockObject);

System.out.println(generalize);
}
}

调试效果:

Java Visualizer 效果:

3.2 原理解析

核心代码:org.apache.dubbo.common.utils.PojoUtils#generalize(java.lang.Object)

public static Object generalize(Object pojo) {
eturn generalize(pojo, new IdentityHashMap());
}

关键代码:

// pojo 待转换的对象
// history 缓存 Map,提高性能
private static Object generalize(Object pojo, Map<Object, Object> history) {
if (pojo == null) {
return null;
}

// 枚举直接返回枚举名
if (pojo instanceof Enum<?>) {
return ((Enum<?>) pojo).name();
}

// 枚举数组,返回枚举名数组
if (pojo.getClass().isArray() && Enum.class.isAssignableFrom(pojo.getClass().getComponentType())) {
int len = Array.getLength(pojo);
String[] values = new String[len];
for (int i = 0; i < len; i++) {
values[i] = ((Enum<?>) Array.get(pojo, i)).name();
}
return values;
}

// 基本类型返回 pojo 自身
if (ReflectUtils.isPrimitives(pojo.getClass())) {
return pojo;
}

// Class 返回 name
if (pojo instanceof Class) {
return ((Class) pojo).getName();
}

Object o = history.get(pojo);
if (o != null) {
return o;
}
history.put(pojo, pojo);

// 数组类型,递归
if (pojo.getClass().isArray()) {
int len = Array.getLength(pojo);
Object[] dest = new Object[len];
history.put(pojo, dest);
for (int i = 0; i < len; i++) {
Object obj = Array.get(pojo, i);
dest[i] = generalize(obj, history);
}
return dest;
}
// 集合类型递归
if (pojo instanceof Collection<?>) {
Collection<Object> src = (Collection<Object>) pojo;
int len = src.size();
Collection<Object> dest = (pojo instanceof List<?>) ? new ArrayList<Object>(len) : new HashSet<Object>(len);
history.put(pojo, dest);
for (Object obj : src) {
dest.add(generalize(obj, history));
}
return dest;
}
// Map 类型,直接 对 key 和 value 处理
if (pojo instanceof Map<?, ?>) {
Map<Object, Object> src = (Map<Object, Object>) pojo;
Map<Object, Object> dest = createMap(src);
history.put(pojo, dest);
for (Map.Entry<Object, Object> obj : src.entrySet()) {
dest.put(generalize(obj.getKey(), history), generalize(obj.getValue(), history));
}
return dest;
}
Map<String, Object> map = new HashMap<String, Object>();
history.put(pojo, map);

// 开启生成 class 则写入 pojo 的class
if (GENERIC_WITH_CLZ) {
map.put("class", pojo.getClass().getName());
}

// 处理 get 方法
for (Method method : pojo.getClass().getMethods()) {
if (ReflectUtils.isBeanPropertyReadMethod(method)) {
ReflectUtils.makeAccessible(method);
try {
map.put(ReflectUtils.getPropertyNameFromBeanReadMethod(method), generalize(method.invoke(pojo), history));
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
// 处理公有属性
for (Field field : pojo.getClass().getFields()) {
if (ReflectUtils.isPublicInstanceField(field)) {
try {
Object fieldValue = field.get(pojo);
// 对象已经解析过,直接从缓存里读提高性能
if (history.containsKey(pojo)) {
Object pojoGeneralizedValue = history.get(pojo);
// 已经解析过该属性则跳过(如公有属性,且有 get 方法的情况)
if (pojoGeneralizedValue instanceof Map
&& ((Map) pojoGeneralizedValue).containsKey(field.getName())) {
continue;
}
}
if (fieldValue != null) {
map.put(field.getName(), generalize(fieldValue, history));
}
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
return map;
}

关键截图

org.apache.dubbo.common.utils.ReflectUtils#getPropertyNameFromBeanReadMethod
public static String getPropertyNameFromBeanReadMethod(Method method) {
if (isBeanPropertyReadMethod(method)) {
// get 方法,则从 index =3 的字符小写 + 后面的字符串
if (method.getName().startsWith("get")) {
return method.getName().substring(3, 4).toLowerCase()
+ method.getName().substring(4);
}
// is 开头方法, index =2 的字符小写 + 后面的字符串
if (method.getName().startsWith("is")) {
return method.getName().substring(2, 3).toLowerCase()
+ method.getName().substring(3);
}
}
return null;
}

因此, getALong 方法对应的属性名被解析为 aLong。

同时,这么处理也会存在问题。如当属性名叫 URL 时,转为 Map 后 key 就会被解析成 uRL。

从这里看出,当属性名比较特殊时也很容易出问题,但 dubbo 这个工具类更符合我们的预期。更多细节,大家可以根据 DEMO 自行调试学习。

如果想严格和属性保持一致,可以使用反射获取属性名和属性值,加缓存机制提升解析的效率。

四、总结

Java Bean 转 Map 的坑很多,最常见的就是类型丢失和属性名解析错误的问题。大家在使用 JSON 框架和 Java Bean 转 Map 的框架时要特别小心。平时使用某些框架时,多写一些 DEMO 进行验证,多读源码,多调试,少趟坑。

文章目录
  1. 1. 一、背景
  2. 2. 二、那些坑
    1. 2.1. 2.0 测试对象
    2. 2.2. 2.1 JSON 反序列化了类型丢失
      1. 2.2.1. 2.1.1 问题复现
      2. 2.2.2. 2.2.2 问题描述
    3. 2.3. 2.2 BeanMap 转换属性名错误
      1. 2.3.1. 2.2.1 commons-beanutils 的 BeanMap
      2. 2.3.2. 2.2.2 使用 cglib 的 BeanMap
  3. 3. 三、解决办法
    1. 3.1. 3.1 解决方案
    2. 3.2. 3.2 原理解析
  4. 4. 四、总结