简体   繁体   中英

Flattening Java Bean to a Map

I am stuck at converting Java Bean to Map . There are many resources on the internet, but unfortunately they all treat converting simple beans to Maps. My ones are a little bit more extensive.

There's simplified example:

public class MyBean {

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

  // getters & setters

}

My point is to produce Map<String, Object> which, in this case, is true for following conditions:

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

I have tried using Apache Commons BeanUtils . Both approaches BeanUtils#describe(Object) and BeanMap(Object) produce a Map which "deep level" is 1 (I mean that there's only "homeAddress" key, holding MyHomeAddress object as a value). My method should enter the objects deeper and deeper until it meets a primitive type (or String), then it should stop digging and insert key ie "order.customer.contactInfo.home" .

So, my question is: how can it be easliy done (or is there already existing project which would allow me to do that)?

update

I have expanded Radiodef answer to include also Collections, Maps Arrays and Enums:

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

Here's a simple reflective/recursive example.

You should be aware that there are some issues with doing a conversion the way you've asked:

  • Map keys must be unique.
  • Java allows classes to name their private fields the same name as a private field owned by an inherited class.

This example doesn't address those because I'm not sure how you want to account for them (if you do). If your beans inherit from something other than Object , you will need to change your idea a little bit. This example only considers the fields of the subclass.

In other words, if you have

public class SubBean extends Bean {

this example will only return fields from SubBean .

Java lets us do this:

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

Not that anybody should be doing that, but it's a problem if you want to use String as the keys, because there would be two keys named "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());
    }
}

You could always use the Jackson Json Processor . Like this:

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

where pojo is some Java bean. You can use some nice annotations on the bean to control the serialization.

You can re-use the ObjectMapper.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM