2014-09-02 19 views
7

Utknąłem przy konwersji Java Bean na Map. Istnieje wiele zasobów w Internecie, ale niestety wszystkie one traktują konwersję prostych komponentów do Map. Moje są nieco bardziej rozległe.Spłaszczanie ziarna Javy na mapie

Jest uproszczony przykład:

public class MyBean { 

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

    // getters & setters 

} 

Chodzi mi o to, aby produkować Map<String, Object>, które w tym przypadku odnosi się do następujących warunków:

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

Próbowałem, używając Apache Commons BeanUtils. Oba podejścia podają BeanUtils#describe(Object) i BeanMap(Object) wytwarzają Mapę, której "głęboki poziom" wynosi 1 (to znaczy, że istnieje tylko klucz "homeAddress", trzymający obiekt MyHomeAddress jako wartość). Moja metoda powinna wejść do obiektów głębiej i głębiej, dopóki nie napotka podstawowego typu (lub ciągu), a następnie powinna przestać kopać i wstawiać klucz, tj. "order.customer.contactInfo.home".

Moje pytanie brzmi: jak można to łatwo zrobić (czy jest już istniejący projekt, który pozwoli mi to zrobić)?

aktualizacja

I Rozszerzyliśmy Radiodef odpowiedź na to także kolekcje, mapy tablice i wyliczenia:

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

Prawdopodobnie nie masz na myśli "prymitywnego", ponieważ 'String' nie jest prymitywny (rozszerza' Object'). Więc potrzebujesz sposobu, aby powiedzieć algorytmowi, które klasy przechodzić i które przyjmować jako wartości, więc prawdopodobnie nie będzie sposobu, aby to zrobić bez jakiejś konfiguracji (może za pomocą adnotacji). – Tonio

+1

Można tego dokonać za pomocą refleksji i rekursji, prawie na pewno trzeba to napisać samemu. Zwróć uwagę, że w tej chwili pytanie zostanie zamknięte, ponieważ prośba o rekomendacje biblioteki nie jest tematem. – Radiodef

+0

Tonio, Radiodef - dziękuję za sugestie, zredagowałem mój post. –

Odpowiedz

5

Oto prosty odblaskowe/rekurencyjne przykładem.

Trzeba mieć świadomość, że istnieją pewne problemy z wykonaniu konwersji drogę pan zapytał:

  • Mapa klucze muszą być unikalne.
  • Java umożliwia klasom nadawanie nazw ich prywatnym polom tej samej nazwy, co prywatne pole należące do odziedziczonej klasy.

Ten przykład nie rozwiązuje tych problemów, ponieważ nie jestem pewien, w jaki sposób chcesz je uwzględnić (jeśli tak). Jeśli twoje fasole dziedziczą z czegoś innego niż Object, będziesz musiał nieco zmienić swój pomysł. Ten przykład uwzględnia tylko pola podklasy.

Innymi słowy, jeśli masz

public class SubBean extends Bean { 

przykład ten powróci tylko pola z SubBean.

Java pozwala nam to zrobić szaloną rzecz:

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

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

Nie, że ktoś powinien tego robić, ale jest to problem, jeśli chcesz używać String jako klucze.

Oto teh codez:

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

public final class BeanFlattener { 
    private BeanFlattener() {} 

    public static Map<String, Object> deepToMap(Object bean) 
    throws IllegalAccessException { 
     Map<String, Object> map = new LinkedHashMap<>(); 

     putValues(bean, map, null); 

     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()) { 
      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<?>> valueClasses = (
     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 
     ))) 
    ); 

    private static boolean isValue(Object value) { 
     return value == null || valueClasses.contains(value.getClass()); 
    } 
} 
+0

Dziękuję, ta implementacja działa jak urok. Rozszerzyłem metodę 'isValue (Object)', aby uwzględnić także inne klasy, które były potrzebne w moim projekcie (zobacz mój post). –

+0

Dziękuję za świetny kawałek kodu @Radiodef. Napisałem kilka testów jednostkowych tego kodu i planuję użyć go w Api, zastanawiałem się, czy ktoś ma do czynienia z jakimś błędem w tym kodzie? – AngelThread

+1

@AngelThread Wystarczy spojrzeć na mój kod tutaj ponownie i jedynym oczywistym problemem jaki widzę jest to, że spowoduje 'StackOverflowError' lub' OutOfMemoryError' jeśli zostanie przekazany obiekt, który zawiera odwołanie cykliczne, na przykład 'class A {B b; } 'i' class B {A a; } '. Taki obiekt może nie być uważany za fasolę, ale gdybym pisał tę metodę, poradziłbym sobie z takimi przypadkami. Myślę, że masz 'Set wartości;' i za każdym razem, gdy dodajesz pole, które nie jest typem wartości, najpierw sprawdź 'Set', aby zobaczyć, czy wartość została już dodana. Jeśli jest w zestawie, nie powtarza się. – Radiodef

1

Zawsze możesz użyć Jackson Json Processor.W ten sposób:

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

gdzie pojo jest jakimś komponentem Java bean. Możesz użyć ładnych adnotacji na fasoli, aby kontrolować serializację.

Możesz ponownie użyć ObjectMapper.

+0

Metoda convertValue() nie spłaszcza obiektów wewnętrznych, na przykład Mam klasę Cat, która ma atrybut jest nazywana przyjacielem i ten atrybut jest także typem Cat. Jeśli skonwertuję instancję klasy Cat za pomocą metody convertValue, będzie ona następująca. {friend = {friend = null, childCount = 2, name = yellow}, childCount = 1, name = red} – AngelThread

+0

@AngelThread Spłaszczanie nie jest wykonywane, ale obiekty wewnętrzne mogą być konwertowane w zależności od ich typu (na przykład wewnętrzne mapy) . I możliwe jest skonfigurowanie Jacksona, aby utworzyć hierarchię mapy, ale to wykracza poza zakres tego pytania. – dlaidlaw

+0

Dziękuję za dalszy wkład w to, starałem się pokazać stronę tego kawałka kodu. – AngelThread