【问题标题】:Flattening Java Bean to a Map将 Java Bean 展平为地图
【发布时间】:2014-10-26 23:39:24
【问题描述】:

我坚持将Java Bean 转换为Map。互联网上有很多资源,但不幸的是,它们都将简单的 bean 转换为 Maps。我的更广泛一些。

有一个简化的例子:

public class MyBean {

  private String firstName;
  private String lastName;
  private MyHomeAddress homeAddress;
  private int age;

  // getters & setters

}

我的意思是产生Map<String, Object>,在这种情况下,以下条件是正确的:

map.containsKey("firstName")
map.containsKey("lastName")
map.containsKey("homeAddress.street")  // street is String
map.containsKey("homeAddress.number")  // number is int
map.containsKey("homeAddress.city")    // city is String
map.containsKey("homeAddress.zipcode") // zipcode is String
map.containsKey("age")

我尝试过使用Apache Commons BeanUtilsBeanUtils#describe(Object)BeanMap(Object) 两种方法都会生成一个“深度”为 1 的 Map(我的意思是只有 "homeAddress" 键,将 MyHomeAddress 对象作为值)。我的方法应该越来越深入地输入对象,直到它遇到原始类型(或字符串),然后它应该停止挖掘并插入密钥,即"order.customer.contactInfo.home"

所以,我的问题是:如何轻松完成(或者是否已经存在可以让我这样做的项目)?

更新

我已扩展 Radiodef 答案以包括集合、地图数组和枚举:

private static boolean isValue(Object value) {
  final Class<?> clazz = value.getClass();
  if (value == null ||
      valueClasses.contains(clazz) ||
      Collection.class.isAssignableFrom(clazz) ||
      Map.class.isAssignableFrom(clazz) ||
      value.getClass().isArray() ||
      value.getClass().isEnum()) {
    return true;
  }
  return false;
}

【问题讨论】:

  • 您可能不是指“原始”,因为String 不是原始的(它扩展了Object)。所以你需要一种方法来告诉算法要遍历哪些类,以及将哪些类作为值,所以如果没有某种配置(可能使用注释),可能没有办法做到这一点。跨度>
  • 这可以通过反射和递归来完成,您几乎可以肯定必须自己编写。请注意,目前该问题将被关闭,因为要求图书馆推荐是题外话。
  • Tonio,Radiodef - 感谢您的建议,我已经编辑了我的帖子。
  • 嗨,通常我会遇到想将 bean 映射到传输对象的情况。当创建页面对象以进行显示或以某种方式将结构从一种更改为另一种时,通常会出现这种情况。在这些情况下,我使用 Dozer 或 ModelMapper 之类的东西,它们允许使用各种方法进行映射,例如 xml、api 或基于注释。
  • @TechTrip 感谢您提到 Dozer 和 ModelMapper。不幸的是,它们都不允许 JavaBean 以 Map 结尾。无论如何,我刚刚读过它们,它们似乎是非常有趣的工具。

标签: java javabeans apache-commons


【解决方案1】:

这是一个简单的反射/递归示例。

您应该知道,按照您要求的方式进行转换存在一些问题:

  • 映射键必须是唯一的。
  • Java 允许类将其私有字段命名为与继承类拥有的私有字段相同的名称。

这个例子没有解决这些问题,因为我不确定你想如何解释它们(如果你这样做的话)。如果你的 bean 继承自 Object 以外的东西,你需要稍微改变你的想法。此示例仅考虑子类的字段。

换句话说,如果你有

public class SubBean extends Bean {

此示例将仅返回来自 SubBean 的字段。

Java 让我们这样做:

package com.acme.util;
public class Bean {
    private int value;
}

package com.acme.misc;
public class Bean extends com.acme.util.Bean {
    private int value;
}

并不是说任何人都应该这样做,但是如果您想使用String 作为键,那就有问题了,因为会有两个名为"value" 的键。

import java.lang.reflect.*;
import java.util.*;

public final class BeanFlattener {
    private BeanFlattener() {}

    public static Map<String, Object> deepToMap(Object bean) {
        Map<String, Object> map = new LinkedHashMap<>();
        try {
            putValues(bean, map, null);
        } catch (IllegalAccessException x) {
            throw new IllegalArgumentException(x);
        }
        return map;
    }

    private static void putValues(Object bean,
                                  Map<String, Object> map,
                                  String prefix)
            throws IllegalAccessException {
        Class<?> cls = bean.getClass();

        for (Field field : cls.getDeclaredFields()) {
            if (field.isSynthetic() || Modifier.isStatic(field.getModifiers()))
                continue;
            field.setAccessible(true);

            Object value = field.get(bean);
            String key;
            if (prefix == null) {
                key = field.getName();
            } else {
                key = prefix + "." + field.getName();
            }

            if (isValue(value)) {
                map.put(key, value);
            } else {
                putValues(value, map, key);
            }
        }
    }

    private static final Set<Class<?>> VALUE_CLASSES =
        Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            Object.class,    String.class, Boolean.class,
            Character.class, Byte.class,   Short.class,
            Integer.class,   Long.class,   Float.class,
            Double.class
            // etc.
        )));

    private static boolean isValue(Object value) {
        return value == null
            || value instanceof Enum<?>
            || VALUE_CLASSES.contains(value.getClass());
    }
}

【讨论】:

  • 谢谢,这个实现就像一个魅力。我已经扩展了isValue(Object) 方法,以包含我的项目中需要的其他类(请参阅我的帖子)。
  • 感谢@Radiodef 提供的精彩代码。我已经为此代码编写了一些单元测试,并且我打算在 Api 中使用它,我想知道是否有人在这段代码中遇到了一些错误?
  • @AngelThread 再次浏览我的代码,我看到的唯一明显问题是,如果传递了一个包含循环引用的对象,它将导致StackOverflowErrorOutOfMemoryError,例如class A { B b; }class B { A a; }。这样的对象可能不被视为 bean,但如果我要重新编写此方法,我会处理这样的情况。我想你会有一个Set&lt;Object&gt; values;,每次你添加一个不是值类型的字段时,你首先检查Set,看看是否已经添加了值。如果它在集合中,则不递归。
  • @Radiodef 感谢您的进一步支持,我认为您示例中的对象同样不能被视为 bean,但我会考虑这样的情况。
【解决方案2】:

您始终可以使用Jackson Json Processor。像这样:

import com.fasterxml.jackson.databind.ObjectMapper;
//...
ObjectMapper objectMapper = new ObjectMapper();
//...
@SuppressWarnings("unchecked")
Map<String, Object> map = objectMapper.convertValue(pojo, Map.class);

其中 pojo 是一些 Java bean。您可以在 bean 上使用一些不错的注解来控制序列化。

您可以重复使用 ObjectMapper。

【讨论】:

  • convertValue() 方法不会展平内部对象,例如我有一个 Cat 类,它有一个名为friend的属性,这个属性也是 Cat 类型。如果我使用 convertValue 方法转换 Cat 类的实例,它将如下所示。 {friend={friend=null, childCount=2, name=yellow}, childCount=1, name=red}
  • @AngelThread 不执行扁平化,但内部对象可能会根据其类型进行转换(例如,内部映射)。并且可以配置 Jackson 来创建 Map 层次结构,但这超出了这个问题的范围。
  • 感谢您对此的进一步贡献,我试图展示这段代码的一面。
  • 我想称赞帖子的作者不仅提供了类的名称,还提供了包的名称。唉,很少有作者这样做。 +1
猜你喜欢
  • 2020-07-06
  • 2017-08-22
  • 1970-01-01
  • 1970-01-01
  • 2017-09-21
  • 2015-12-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多