2014-09-02 16 views
7

Sono bloccato alla conversione di Java Bean in Map. Ci sono molte risorse su Internet, ma sfortunatamente trattano tutti la conversione di bean semplici in Maps. I miei sono un po 'più ampi.Appiattimento del bean Java su una mappa

C'è esempio semplificato:

public class MyBean { 

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

    // getters & setters 

} 

Il mio punto è quello di produrre Map<String, Object> che, in questo caso, è vero per seguenti condizioni:

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

Ho provato con Apache Commons BeanUtils. Entrambi gli approcci BeanUtils#describe(Object) e BeanMap(Object) producono una mappa che "livello profondo" è 1 (intendo che è presente solo la chiave "homeAddress", contenente l'oggetto MyHomeAddress come valore). Il mio metodo dovrebbe inserire gli oggetti sempre più in profondità fino a quando non incontra un tipo primitivo (o String), quindi dovrebbe smettere di scavare e inserire la chiave, ovvero "order.customer.contactInfo.home".

Quindi, la mia domanda è: come può essere fatto facilmente (o esiste già un progetto che mi consenta di farlo)?

aggiornamento

ho ampliato Radiodef risposta per includere anche collezioni, Mappe array e enumerazioni:

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

Probabilmente non si intende "primitivo", poiché 'String' non è un primitivo (estende' Object'). Quindi ti serve un modo per dire all'algoritmo quali classi attraversare e quali prendere come valori, quindi probabilmente non ci sarà un modo per farlo senza una sorta di configurazione (magari usando le annotazioni). – Tonio

+1

Questo può essere fatto con la riflessione e la ricorsione, quasi sicuramente devi scriverlo da solo. Nota che al momento la domanda verrà chiusa perché chiedere consigli per le biblioteche è fuori tema. – Radiodef

+0

Tonio, Radiodef - grazie per i vostri suggerimenti, ho modificato il mio post. –

risposta

5

Ecco un semplice/esempio ricorsivo riflettente.

È necessario essere consapevoli che ci sono alcuni problemi con un'operazione di conversione il modo che hai chiesto:

  • chiavi Mappa deve essere univoco.
  • Java consente alle classi di denominare i propri campi privati ​​lo stesso nome di un campo privato di proprietà di una classe ereditata.

Questo esempio non si rivolge a quelli perché non sono sicuro di come si desidera renderli conto (se lo si fa). Se i tuoi bean ereditano qualcosa di diverso da Object, dovrai cambiare un po 'la tua idea. Questo esempio considera solo i campi della sottoclasse.

In altre parole, se avete

public class SubBean extends Bean { 

questo esempio restituisce solo i campi da SubBean.

Java permette di fare questa cosa folle:

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

Non che qualcuno dovrebbe fare questo, ma è un problema se si desidera utilizzare String come le chiavi.

Ecco 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

Grazie, questa implementazione funziona come un fascino. Ho ampliato il metodo 'isValue (Object)' per includere anche altre classi che erano necessarie nel mio progetto (vedi il mio post). –

+0

Grazie per il gran pezzo di codice @Radiodef. Ho scritto alcuni test unitari per questo codice, e sto pensando di usarlo in una API, mi stavo chiedendo che qualcuno faccia qualche errore in questo stesso codice? – AngelThread

+1

@AngelThread Semplicemente rileggendo il mio codice qui di nuovo e l'unico problema ovvio che vedo è che causerà un 'StackOverflowError' o' OutOfMemoryError' se ha passato un oggetto che contiene un riferimento circolare, ad esempio 'classe A {B b; } 'e' classe B {A a; } '. Tale oggetto potrebbe non essere considerato un bean, ma se dovessi scrivere di nuovo questo metodo, gestirò casi del genere. Penso che avresti un 'Set valori;' e ogni volta che aggiungi un campo che non è un tipo di valore devi prima controllare il 'Set' per vedere se il valore è già stato aggiunto. Se è nel set, non si recita. – Radiodef

1

Si può sempre utilizzare il Jackson Json Processor.In questo modo:

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

dove pojo è un po 'di bean Java. Puoi usare alcune belle annotazioni sul bean per controllare la serializzazione.

È possibile riutilizzare ObjectMapper.

+0

Il metodo convertValue() non appiattisce gli oggetti interni, ad esempio ho una classe Cat che ha un attributo chiamato friend e questo attributo è anche di tipo Cat. Se converto istanza di classe Cat con il metodo convertValue, sarà come seguire. {amico = {amico = null, childCount = 2, name = giallo}, childCount = 1, name = red} – AngelThread

+0

@AngelThread L'appiattimento non viene eseguito, ma gli oggetti interni possono essere convertiti in base al loro tipo (ad esempio, mappe interne) . Ed è possibile configurare Jackson per creare una gerarchia di mappe, ma questo è al di fuori dello scopo di questa domanda. – dlaidlaw

+0

Grazie per ulteriore contributo su questo, stavo cercando di mostrare un lato di questo pezzo di codice. – AngelThread

Problemi correlati