Das Klassendiagramm für den Java Bean Mapper Test am Beispiel "ByHand"

Ich hatte ein Problem. Ich hatte einen Webservice zu implementieren.

Dabei musste ich die interne Objekthierarchie einer Anwendung auf ca. 23 Servicemethoden mappen. Zusätzlich musste der Webservice aus einem komplexen Objektgeflecht bestückt werden.

Hätte ich dieses Mapping von Hand programmiert, wäre ich Wochen damit beschäftigt gewesen. Eine Lösung dieses Problems sind die Java Bean Mapper.

In diesem Beitrag zeige ich, welche Java Bean Mapper es gibt und welche Probleme damit gelöst bzw. aufgetreten sind.

Zusätzlich habe ich die Performanz der Java Bean Mappers miteinander verglichen.

Die Aufgabe

… ist ein typisches Problem aus der Praxis. Das Mappen von strukturierten Daten an Schnittstellen bzw. in Endpoints.

Dort werden die Daten der Schnittstelle auf die interne Objekthierarchie der Anwendung gemappt.

Dabei können bei einer umfangreichen Schnittstelle mit einem komplexen Sachverhalt recht aufwändige Objektgeflechte entstehen. Erschwerend kommt noch die Verschiedenartigkeit der aneinandergrenzenden Anwendungsschichten (Persistenz-, Domain-, Service-, REST- oder Web-API) hinzu.

Werden diese Objektgeflechte von Hand gemappt, sind Fehler vorprogrammiert. Daher wird immer die Frage nach einem automatischen Mapping gestellt.

Zur Lösung des Problems fiel meine Wahl auf den Klassiker – Dozer.

Leider stellte sich recht schnell heraus, dass die Performanz dieses Mappers nicht ausreichend für die Lösung des Problems war.

Probleme über Probleme

Bei der Suche nach alternativen Java Bean Mapper traten weitere Hindernisse auf.

  • Einige Mapper kamen mit den generierten JAXB-Klassen nicht zurecht, da die JAXB-Klassen nicht nach dem Java Bean Standard generiert werden. Die Collections haben keinen Setter Methoden und die Getter Methoden liefern immer eine Collection zurück.
public class XmlTable implements Serializable {
    protected List<XmlRow> rows;

    public List<XmlRow> getRows() {
        if (rows == null) {
            rows = new ArrayList<XmlRow>();
        }
        return this.rows;
    }

}
  • Auch die erweiterte Generierung der JAXB-Klassen mit dem XJC-Plugin jaxb2-basics half nicht weiter.
  • Dieses Plugin generiert für die Collections Setter Methoden. Aber bei einem nicht gesetzten Property wird die Setter Methode mit null aufgerufen. Daran stört sich aber die Methode addAll().
    public void setRows(List<XmlRow> value) {
        this.rows = null;
        List<XmlRow> draftl = this.getRows();
        draftl.addAll(value);
    }
  • Einige Mapper können nicht rekursiv durch die Objekthierarchie iterieren.
  • Somit wurden die in Collections enthaltenen Objekte nicht gemappt – sondern nur kopiert. Dadurch landeten nicht die erwarteten Klassen in den gemappten Objekten.
  • Ein Beispiel ist das Mappen einer DomainTable auf eine XmlTable. Beide Klassen haben ein Property rows. Im Falle der DomainTable ist das Property vom Typ List<DomainRow> rows; und der XmlTable vom Typ List<XmlRow> rows;. Nach dem Mappen von DomainTable auf die XmlTable befindet sich im Property Objekte vom Typ DomainRow.

Die Testimplementation

Wie hab ich den Test aufgebaut?

Der Test besteht aus zwei Testläufen, die die obengenannten Probleme berücksichtigt.

  1. Die Objekthierarchie wird vollständig mit Daten gefüllt.
    Leider können nicht alle Java Bean Mapper bei diesem Test eingeschossen werden. Die Mapper, die die obengenannten Probleme haben, wurden hier nicht berücksichtigt.
  2. Die Objekthierarchie wird teilweise mit Daten gefüllt.
    Bei diesem einfachen Test konnten alle Java Bean Mapper berücksichtigt werden. Allerdings ist die Aussagefähigkeit dieses Tests in der Praxis fraglich, da wesentliche Funktionen fehlen.

Die Links zum Quellcode finden Sie weiter unten.

Die Objekthierarchie

In diesem Beispiel werden zwei Objekthierarchien (JAXB– und JPA-Klassen) aufeinander gemappt.

Die Eingangsdatenstruktur wurde aus der WSDL (https://xmlns.frank-rahn.de/ws/test/1.0) per JAXB generiert.

Die Ausgangsdatenstruktur besteht aus JPA Entities.

In den folgenden UML-Diagrammen sind die Objekthierarchien der JAXB– und JPA-Klassen dargestellt.

Der JUnit Test

In der Klasse AbstractPerformanceTest wird in der Methode runTestWithMapper() für jeden Java Bean Mapper das Mapping durchgeführt. Für jeden Durchlauf werden die Daten erfasst. Vor jeder Messreihe werden alle Mapper 10-mal aufgerufen, um den Test bzw. die Implementationen einzuschwingen. Der eigentliche Test läuft dann 10 Minuten.

Es wird die durchschnittliche Dauer in Abhängigkeit von der Anzahl der Durchläufe ermittelt.

Zusätzlich werden folgende Kenndaten über die Dauer ermittelt.

  • Minimaler Wert
  • Maximaler Wert
  • Letzter gemessener Wert
  • Arithmetischer Mittelwert
  • Standardabweichung

Die Ergebnisse werden in einem Excel-Sheet dargestellt. Die Dauer wird in Millisekunden angegeben.

Im folgenden Klassendiagramm ist die Objekthierarchie des JUnit-Tests dargestellt.

Das Klassendiagramm für den Java Bean Mapper Test am Beispiel 'ByHand' (© Frank Rahn)

Die getesteten Java Bean Mapper mit Bewertung und Hinweise zur Implementierung

Bei der Implementierung habe ich darauf geachtet, dass keine Technik zum Einsatz kommt, die eine direkte Änderung des Quellcodes der JAXB- bzw. JPA-Klassen erzwungen hätte. Z. B. durch den Einsatz einer speziellen Annotation eines Mapper Frameworks.

Die folgenden Java Bean Mapper wurden von mir untersucht.

ByHand

Der Java Bean Mapper ByHand ist kein automatisch generierter Mapper.

Dieser Java Bean Mapper ist eine Implementierung von Hand (hand coded). Hierbei wird jedes einzelne Property von einem Java Bean in ein anderes Java Bean, ggf. mit Typkonvertierung, übertragen.

Das Erstellen dieses Mappers ist sehr zeitintensiv und fehleranfällig. Eigentlich eine Aufgabe für einen dressierten Affen. Allerdings können alle fachlichen Aspekte individuell berücksichtigt werden.

Diese Art von Mappern liefert die beste Performanz.

/**
 * Der Mapper per "ByHand".
 *
 * @author Frank W. Rahn
 */
@Component("ByHand")
@Order(0)
public class ByHandTestBeansMapperBean extends AbstractTestBeansMapperBean {

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return map(createXmlTable(), source);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return map(new DomainTable(), source);
  }

  private DomainRow map(DomainRow target, XmlRow source) {
    // Mapping String
    target.setColumn00(source.getColumn00());
    target.setColumn01(source.getColumn01());
    target.setColumn02(source.getColumn02());
    target.setColumn03(source.getColumn03());
    target.setColumn04(source.getColumn04());
    target.setColumn05(source.getColumn05());
    target.setColumn06(source.getColumn06());
    target.setColumn07(source.getColumn07());
    target.setColumn08(source.getColumn08());
    target.setColumn09(source.getColumn09());

    // Mapping int
    target.setColumn10(source.getColumn10());
    target.setColumn11(source.getColumn11());
    target.setColumn12(source.getColumn12());
    target.setColumn13(source.getColumn13());
    target.setColumn14(source.getColumn14());
    target.setColumn15(source.getColumn15());
    target.setColumn16(source.getColumn16());
    target.setColumn17(source.getColumn17());
    target.setColumn18(source.getColumn18());
    target.setColumn19(source.getColumn19());

    // Mapping boolean
    target.setColumn20(source.isColumn20());
    target.setColumn21(source.isColumn21());
    target.setColumn22(source.getColumn22());
    target.setColumn23(source.isColumn23());
    target.setColumn24(source.isColumn24());
    target.setColumn25(source.isColumn25());
    target.setColumn26(source.isColumn26());
    target.setColumn27(source.isColumn27());
    target.setColumn28(source.isColumn28());
    target.setColumn29(source.isColumn29());

    // Mapping long
    target.setColumn30(source.getColumn30());
    target.setColumn31(source.getColumn31());
    target.setColumn32(source.getColumn32());
    target.setColumn33(source.getColumn33());
    target.setColumn34(source.getColumn34());
    target.setColumn35(source.getColumn35());
    target.setColumn36(source.getColumn36());
    target.setColumn37(source.getColumn37());
    target.setColumn38(source.getColumn38());
    target.setColumn39(source.getColumn39());

    // Mapping BigDecimal
    target.setColumn40(source.getColumn40());
    target.setColumn41(source.getColumn41());
    target.setColumn42(source.getColumn42());
    target.setColumn43(source.getColumn43());
    target.setColumn44(source.getColumn44());
    target.setColumn45(source.getColumn45());
    target.setColumn46(source.getColumn46());
    target.setColumn47(source.getColumn47());
    target.setColumn48(source.getColumn48());
    target.setColumn49(source.getColumn49());

    // Mapping Calendar
    target.setColumn50(source.getColumn50());
    target.setColumn51(source.getColumn51());
    target.setColumn52(source.getColumn52());
    target.setColumn53(source.getColumn53());
    target.setColumn54(source.getColumn54());
    target.setColumn55(source.getColumn55());
    target.setColumn56(source.getColumn56());
    target.setColumn57(source.getColumn57());
    target.setColumn58(source.getColumn58());
    target.setColumn59(source.getColumn59());

    // Mapping String
    target.setColumn60(source.getColumn60());
    target.setColumn61(source.getColumn61());
    target.setColumn62(source.getColumn62());
    target.setColumn63(source.getColumn63());
    target.setColumn64(source.getColumn64());
    target.setColumn65(source.getColumn65());
    target.setColumn66(source.getColumn66());
    target.setColumn67(source.getColumn67());
    target.setColumn68(source.getColumn68());
    target.setColumn69(source.getColumn69());

    // Mapping int
    target.setColumn70(source.getColumn70());
    target.setColumn71(source.getColumn71());
    target.setColumn72(source.getColumn72());
    target.setColumn73(source.getColumn73());
    target.setColumn74(source.getColumn74());
    target.setColumn75(source.getColumn75());
    target.setColumn76(source.getColumn76());
    target.setColumn77(source.getColumn77());
    target.setColumn78(source.getColumn78());
    target.setColumn79(source.getColumn79());

    // Mapping String[]
    if (source.getColumns() != null) {
      target.setColumns(new ArrayList<>(source.getColumns()));
    }

    return target;
  }

  private DomainTable map(DomainTable target, XmlTable source) {
    target.setName(source.getName());

    if (source.getDate() != null) {
      target.setDate(source.getDate().getTime());
    }

    if (source.getRows().isEmpty()) {
      target.setRows(new ArrayList<DomainRow>());
    } else {
      List<DomainRow> rows = new ArrayList<>();
      for (XmlRow xmlRow : source.getRows()) {
        rows.add(map(new DomainRow(), xmlRow));
      }
      target.setRows(rows);
    }

    return target;
  }

  private XmlRow map(XmlRow target, DomainRow source) {
    // Mapping String
    target.setColumn00(source.getColumn00());
    target.setColumn01(source.getColumn01());
    target.setColumn02(source.getColumn02());
    target.setColumn03(source.getColumn03());
    target.setColumn04(source.getColumn04());
    target.setColumn05(source.getColumn05());
    target.setColumn06(source.getColumn06());
    target.setColumn07(source.getColumn07());
    target.setColumn08(source.getColumn08());
    target.setColumn09(source.getColumn09());

    // Mapping int
    target.setColumn10(source.getColumn10());
    target.setColumn11(source.getColumn11());
    target.setColumn12(source.getColumn12());
    target.setColumn13(source.getColumn13());
    target.setColumn14(source.getColumn14());
    target.setColumn15(source.getColumn15());
    target.setColumn16(source.getColumn16());
    target.setColumn17(source.getColumn17());
    target.setColumn18(source.getColumn18());
    target.setColumn19(source.getColumn19());

    // Mapping boolean
    target.setColumn20(source.isColumn20());
    target.setColumn21(source.isColumn21());
    target.setColumn22(source.getColumn22());
    target.setColumn23(source.isColumn23());
    target.setColumn24(source.isColumn24());
    target.setColumn25(source.isColumn25());
    target.setColumn26(source.isColumn26());
    target.setColumn27(source.isColumn27());
    target.setColumn28(source.isColumn28());
    target.setColumn29(source.isColumn29());

    // Mapping long
    target.setColumn30(source.getColumn30());
    target.setColumn31(source.getColumn31());
    target.setColumn32(source.getColumn32());
    target.setColumn33(source.getColumn33());
    target.setColumn34(source.getColumn34());
    target.setColumn35(source.getColumn35());
    target.setColumn36(source.getColumn36());
    target.setColumn37(source.getColumn37());
    target.setColumn38(source.getColumn38());
    target.setColumn39(source.getColumn39());

    // Mapping BigDecimal
    target.setColumn40(source.getColumn40());
    target.setColumn41(source.getColumn41());
    target.setColumn42(source.getColumn42());
    target.setColumn43(source.getColumn43());
    target.setColumn44(source.getColumn44());
    target.setColumn45(source.getColumn45());
    target.setColumn46(source.getColumn46());
    target.setColumn47(source.getColumn47());
    target.setColumn48(source.getColumn48());
    target.setColumn49(source.getColumn49());

    // Mapping Calendar
    target.setColumn50(source.getColumn50());
    target.setColumn51(source.getColumn51());
    target.setColumn52(source.getColumn52());
    target.setColumn53(source.getColumn53());
    target.setColumn54(source.getColumn54());
    target.setColumn55(source.getColumn55());
    target.setColumn56(source.getColumn56());
    target.setColumn57(source.getColumn57());
    target.setColumn58(source.getColumn58());
    target.setColumn59(source.getColumn59());

    // Mapping String
    target.setColumn60(source.getColumn60());
    target.setColumn61(source.getColumn61());
    target.setColumn62(source.getColumn62());
    target.setColumn63(source.getColumn63());
    target.setColumn64(source.getColumn64());
    target.setColumn65(source.getColumn65());
    target.setColumn66(source.getColumn66());
    target.setColumn67(source.getColumn67());
    target.setColumn68(source.getColumn68());
    target.setColumn69(source.getColumn69());

    // Mapping int
    target.setColumn70(source.getColumn70());
    target.setColumn71(source.getColumn71());
    target.setColumn72(source.getColumn72());
    target.setColumn73(source.getColumn73());
    target.setColumn74(source.getColumn74());
    target.setColumn75(source.getColumn75());
    target.setColumn76(source.getColumn76());
    target.setColumn77(source.getColumn77());
    target.setColumn78(source.getColumn78());
    target.setColumn79(source.getColumn79());

    // Mapping String[]
    if (source.getColumns() == null) {
      target.setColumns(null);
    } else {
      target.setColumns(new ArrayList<>(source.getColumns()));
    }

    return target;
  }

  private XmlTable map(XmlTable target, DomainTable source) {
    target.setName(source.getName());

    if (source.getDate() != null) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(source.getDate());
      target.setDate(calendar);
    }

    if (source.getRows() != null) {
      for (DomainRow domainRow : source.getRows()) {
        target.getRows().add(map(createXmlRow(), domainRow));
      }
    }

    return target;
  }

}

Apache Commons BeanUtils

Dieser Java Bean Mapper ist Teil der Bibliothek Apache Commons BeanUtils der Apache Software Foundation. Dieses Projekt stellt nützliche Tools rund um den Standard JavaBeans Component API java.beans.Introspector zu Verfügung und verwendet zum Mappen Reflection (reflection-based).

Der Mapper besteht aus der Klasse org.apache.commons.beanutils.BeanUtils. Dieser Mapper kann das zumappende Objekt nicht rekursive bearbeiten und ist auf die Setter Methode angewiesen – ohne diese kann er die Werte nicht mappen.

Daher kann er nur für einfache und nicht tiefgreifende Mappings verwendet werden.

In diesen Fällen allerdings liefert er eine gute Performanz und ist sehr einfach eingesetzt.

/**
 * Der Mapper für {@link BeanUtils}.
 *
 * @author Frank W. Rahn
 */
@Component("Commons-BeanUtils")
@Order(1)
public class CommonsBeanUtilsTestBeansMapperBean extends AbstractTestBeansMapperBean {

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    XmlTable target = createXmlTable();
    BeanUtils.copyProperties(target, source);
    return target;
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    DomainTable target = new DomainTable();
    BeanUtils.copyProperties(target, source);
    return target;
  }

}

Spring Framework BeanUtils

Dieser Java Bean Mapper ist Teil vom Spring Framework und wird intern vom Spring Framework selber verwendet.

Der Mapper besteht aus der Klasse org.springframework.beans.BeanUtils. Diese Klasse nutzt den Standard JavaBeans Component API java.beans.Introspector und verwendet Reflection (reflection-based).

Dieser Mapper arbeitet ähnlich wie die Apache Common BeanUtils, hat die gleichen Probleme und liefert die gleiche Performanz.

Update (01.01.2020): Seit der Spring Framework Version 5.3 hat sich das Vorgehen beim Mappen von Collection geändert. Die generischen Typinformationen werden jetzt berücksichtigt. Dadurch werden Collection nur noch gemappt, wenn der generische Typ passt. Vorher war die Klasse der Collection entscheidend.

/**
 * Der Mapper für {@link BeanUtils}.
 *
 * @author Frank W. Rahn
 */
@Component("Spring-BeanUtils")
@Order(2)
public class SpringBeanUtilsTestBeansMapperBean extends AbstractTestBeansMapperBean {

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    XmlTable target = createXmlTable();
    BeanUtils.copyProperties(source, target);
    return target;
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    DomainTable target = new DomainTable();
    BeanUtils.copyProperties(source, target);
    return target;
  }

}

Dozer

Der Java Bean Mapper Dozer ist ein komplettes Mapping Framework.

Dozer verwendet den Standard JavaBeans Component API java.beans.Introspector und Reflection (reflection-based) zum Erzeugen der Mapper und kann die Objekte rekursiv durcharbeiten.

Dozer kann über eine XML-Datei umfangreich konfiguriert werden. Die Möglichkeiten der Konfiguration dieses Mappers lassen keine Wünsche offen.

Wie beim Mapper ByHand können alle individuellen fachlichen Aspekte berücksichtigt werden. Die Konfiguration geht schnell vonstatten.

Leider liefert dieser Mapper aber auch die schlechteste Performanz.

/**
 * Der Mapper für {@link DozerBeanMapper}.
 *
 * @author Frank W. Rahn
 */
@Component("Dozer")
@Order(3)
public class DozerTestBeansMapperBean extends AbstractTestBeansMapperBean {

  private Mapper dozer;

  /**
   * Initialisiere diese Spring-Bean.
   */
  @PostConstruct
  public void initialize() {
    dozer = new DozerBeanMapper();
    // Bekannt geben, das die Xml*-Klassen JAXB Objekte sind
    ((DozerBeanMapper) dozer).addMapping(new BeanMappingBuilder() {

      /**
       * {@inheritDoc}
       *
       * @see BeanMappingBuilder#configure()
       */
      @Override
      protected void configure() {
        mapping(new TypeDefinition(DomainTable.class),
          new TypeDefinition(XmlTable.class).beanFactory(JAXBBeanFactory.class));
      }
    });
    ((DozerBeanMapper) dozer).addMapping(new BeanMappingBuilder() {

      /**
       * {@inheritDoc}
       *
       * @see BeanMappingBuilder#configure()
       */
      @Override
      protected void configure() {
        mapping(new TypeDefinition(DomainRow.class),
          new TypeDefinition(XmlRow.class).beanFactory(JAXBBeanFactory.class));
      }
    });
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return dozer.map(source, XmlTable.class);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return dozer.map(source, DomainTable.class);
  }

}

Orika

Der Java Bean Mapper Orika ist ähnlich wie Dozer ein Mapping Framework.

Im Grunde stellt Orika einen Replacment von Dozer mit gleichem Funktionsumfang dar.

Allerdings verwendet Orika die Bytecode Generierung von Javassist zum Erzeugen der Mapper. Dadurch hat Orika, bei gleichem Funktionsumfang, eine deutlich bessere Performanz als Dozer. Allerdings können die generierten Mapper nicht debuggt werden.

/**
 * Der Mapper für {@link MapperFacade}.
 *
 * @author Frank W. Rahn
 */
@Component("Orika")
@Order(4)
public class OrikaTestBeansMapperBean extends AbstractTestBeansMapperBean {

  private MapperFacade orika;

  /**
   * Initialisiere diese Spring-Bean.
   */
  @PostConstruct
  public void initialize() {
    MapperFactory factory = new DefaultMapperFactory.Builder().build();
    orika = factory.getMapperFacade();
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return orika.map(source, XmlTable.class);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return orika.map(source, DomainTable.class);
  }

}

MapStruct

Der Java Bean Mapper MapStruct ist auch ein Mapping Framework.

Allerdings geht dieser Mapper einen kompletten neuen Weg.

Er verwendet einen Annotation Processor (APT) zum Erzeugen von Mappern für die einzelnen Java Beans. Diese generierten Mapper sind richtige Java-Klassen – können also im Fehlerfall auch debuggt werden.

Als Ausgangsbasis dient dem Prozessor ein Interface oder eine abstrakte Basisklasse.

Falls Sie sich für eine abstrakte Basisklasse entscheiden, kann diese Klasse eine individuelle Implementierungen liefern. Damit erreicht dieses Mapping den vollständigen Funktionsumfang wie der Mapper ByHand. Allerdings auch zu einem höheren Implementierungsaufwand im Gegensatz zu Dozer oder Orika.

Diese Interfaces müssen geschrieben werden. Bei einer umfangreichen Objekthierarchie benötigt das Schreiben der Interfaces seine Zeit.

Die dadurch gewonnene Performanz hebt diesen Nachteil allerdings vollständig auf. Dieser Mapper wurde nur geringfügig (im Bereich von Millisekunden) durch den Mapper ByHand geschlagen. Der wesentlich mühseliger zu programmieren ist.

/**
 * Der Mapper für MapStruct.
 *
 * @author Frank W. Rahn
 */
@Component("MapStruct")
@Order(5)
public class MapStructTestBeansMapperBean extends AbstractTestBeansMapperBean {

  @Autowired
  private TestBeansMapper testBeansMapper;

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return testBeansMapper.domainTableToXmlTable(source);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return testBeansMapper.xmlTableToDomainTable(source);
  }

}

ModelMapper

Die Berücksichtigung des Java Bean Mapper ModelMapper wurde in den Kommentaren gewünscht.

Dieser Mapper ähnelt in der Funktionsweise den schon vorher beschriebenen Mappern Orika und Dozer. Dieser Mapper verwendet die Bytecode Generierung von cglib. Das Debuggen der erzeugten Mapper gestaltet sich dadurch schwierig.

Dieser Mapper bietet eine API für spezifische Anwendungsfälle an.

/**
 * Der Mapper für {@link ModelMapper}.
 *
 * @author Frank W. Rahn
 */
@Component("ModelMapper")
@Order(6)
public class ModelMapperTestBeansMapperBean extends AbstractTestBeansMapperBean {

  private ModelMapper modelMapper;

  /**
   * Initialisiere diese Spring-Bean.
   */
  @PostConstruct
  public void initialize() {
    modelMapper = new ModelMapper();
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return modelMapper.map(source, XmlTable.class);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return modelMapper.map(source, DomainTable.class);
  }

}

JMapper Framework

Die Berücksichtigung des Java Bean Mapper JMapper Framework wurde in den Kommentaren gewünscht.

Dieser Mapper ähnelt einwenig dem Mapper MapStruct. Auch bei diesem Mapper muss zwingend eine Konfiguration erstellt werden. Beim JMapper kann zwischen drei Konfigurationsvarianten gewählt werden.

  • Der Verwendung von Annotationen an den Klassen
  • Eine Konfiguration basierend auf einer XML Datei
  • Der JMapper API, welche eine programmatische Konfiguration ermöglicht

Ich habe mich in diesem Beispiel für die Variante JMapper API entschieden.

Der JMapper verwendet die Bytecode Generierung von Javassist zum Erzeugen der Mapper. Das Debuggen der erzeugten Mapper gestaltet sich dadurch schwierig.

Die Performanz dieses Mappers ist beeindruckend. Er kann locker mit dem Mapper ByHand mithalten und ist einfacher umzusetzen als der Mapper MapStruct.

Weitere Features dieses Mappers sind …

  • Relationales Mapping von mehreren Klassen auf eine Klasse (Many to One) und umgekehrt (One to Many)
  • Vererbung von Konfigurationen
/**
 * Der Mapper für {@link JMapper}.
 *
 * @author Frank W. Rahn
 */
@Component("JMapper")
@Order(7)
public class JMapperTestBeansMapperBean extends AbstractTestBeansMapperBean {

  private JMapper<XmlTable, DomainTable> domainToXmlMapper;

  private JMapper<DomainTable, XmlTable> xmlToDomainMapper;

  /**
   * Initialisiere diese Spring-Bean.
   */
  @PostConstruct
  public void initialize() {
    JMapperAPI jMapperAPI = new JMapperAPI()
      .add(mappedClass(XmlTable.class).add(global())
        .add(conversion("dateToCalendar").from("date").to("date")
          .body("java.util.Calendar c = java.util.Calendar.getInstance(); c.setTime(${source}); return c;")))
      .add(mappedClass(XmlRow.class).add(global()))
      .add(mappedClass(DomainTable.class).add(global().excludedAttributes("id"))
        .add(conversion("calendarToDate").from("date").to("date").body("return ${source}.getTime();"))
        .add(attribute("rows").value("rows").targetClasses(DomainRow.class)))
      .add(mappedClass(DomainRow.class).add(global().excludedAttributes("id")));

    domainToXmlMapper = new JMapper<>(XmlTable.class, DomainTable.class, jMapperAPI);
    xmlToDomainMapper = new JMapper<>(DomainTable.class, XmlTable.class, jMapperAPI);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return domainToXmlMapper.getDestination(source);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return xmlToDomainMapper.getDestination(source);
  }

}

Selma

Die Berücksichtigung des Java Bean Mapper Selma wurde in den Kommentaren gewünscht.

Dieser Mapper verwendet einen Annotation Processor (APT) zum Erzeugen der Mapper und ähnelt damit dem schon vorher beschriebenen Mapper MapStruct.

Im Gegensatz zu MapStruct ist der Aufwand für die Implementierung etwas geringer, da nicht für jedes Mapping eine neue Methode definiert werden muss.

/**
 * Der Mapper für {@link Selma}.
 *
 * @author Frank W. Rahn
 */
@Component("Selma")
@Order(8)
public class SelmaTestBeansMapperBean extends AbstractTestBeansMapperBean {

  @Autowired
  private TestBeansMapper mapper;

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return mapper.asXmlTable(source);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return mapper.asDomainTable(source);
  }

}

Update (03.01.2020): Seit dem Umstieg auf das OpenJDK v11 werden in Eclipse v4.16.0 zwei Fehler angezeigt – trotzdem wird der Mapper generiert. Maven liefert dazu keine Meldungen. Diese Meldungen können ignoriert werden.

Error while searching builder for field name on InOutType{in=de.rahn.performance.testbeans.DomainTable, out=https.xmlns_frank_rahn_de.types.testtypes._1.XmlTable, outPutAsParam=false} mapper: java.lang.NullPointerException
Error while searching builder for field name on InOutType{in=https.xmlns_frank_rahn_de.types.testtypes._1.XmlTable, out=de.rahn.performance.testbeans.DomainTable, outPutAsParam=false} mapper: java.lang.NullPointerException

reMap

Die Berücksichtigung des Java Bean Mapper reMap wurde in den Kommentaren gewünscht.

reMap verwendet die Bytecode Generierung von cglib zur Erzeugung von Mappern. Das Debuggen der erzeugten Mapper gestaltet sich dadurch schwierig.

Der Schwerpunkt von reMap liegt in der Robustheit und einem minimalen Verwaltungsaufwand für Tests.

Durch das Hauptaugenmerk auf Robustheit müssen mehr Angaben zum Mapping gemacht werden, als bei anderen Mappern nötig ist. Aber es ist gewollt, dass hier wenige Automatik greift und einiges dem Compiler überlassen wird.

/**
 * Der Mapper für {@link Mapper}.
 *
 * @author Tom Hombergs
 */
@Component("reMap")
@Order(9)
public class ReMapTestBeansMapperBean implements TestBeansMapperBean {

  protected Mapper<DomainTable, XmlTable> domainToXmlTableMapper;

  protected Mapper<XmlTable, DomainTable> xmlToDomainTableMapper;

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#getMapperName()
   */
  @Override
  public String getMapperName() {
    return "reMap";
  }

  /**
   * Initialisiere diese Spring-Bean.
   */
  @PostConstruct
  public ReMapTestBeansMapperBean initialize() {
    Mapper<DomainRow, XmlRow> domainToXmlRowMapper = Mapping.from(DomainRow.class).to(XmlRow.class).mapper();
    Mapper<XmlRow, DomainRow> xmlToDomainRowMapper = Mapping.from(XmlRow.class).to(DomainRow.class).mapper();

    // @formatter:off
    domainToXmlTableMapper = Mapping
      .from(DomainTable.class)
      .to(XmlTable.class)
      .replace(DomainTable::getDate, XmlTable::getDate)
        .with(dateToCalendar())
      .useMapper(domainToXmlRowMapper)
      .mapper();

    xmlToDomainTableMapper = Mapping
      .from(XmlTable.class)
      .to(DomainTable.class)
      .replace(XmlTable::getDate, DomainTable::getDate)
        .with(calendarToDate())
      .useMapper(xmlToDomainRowMapper).mapper();
    // @formatter:on

    return this;
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(DomainTable)
   */
  @Override
  public XmlTable map(DomainTable source) throws Exception {
    return domainToXmlTableMapper.map(source);
  }

  /**
   * {@inheritDoc}
   *
   * @see TestBeansMapperBean#map(XmlTable)
   */
  @Override
  public DomainTable map(XmlTable source) throws Exception {
    return xmlToDomainTableMapper.map(source);
  }

  /**
   * @return ein Transformer von {@link Date} nach {@link Calendar}
   */
  protected Function<Date, Calendar> dateToCalendar() {
    return source -> {
      if (source == null) {
        return null;
      }

      Calendar c = Calendar.getInstance();
      c.setTime(source);
      return c;
    };
  }

  /**
   * @return ein Transformer von {@link Calendar} nach {@link Date}
   */
  protected Function<Calendar, Date> calendarToDate() {
    return source -> {
      if (source == null) {
        return null;
      }

      return source.getTime();
    };
  }

}

Im folgenden Quellcode die Beispiele für die Tests des Mappers.

/**
 * Test des Mappers für reMap.
 *
 * @author Tom Hombergs
 */
public class ReMapTestBeansMapperBeanTest extends AbstractTestBeansMapperBeanTest {

  /**
   * @throws java.lang.Exception
   */
  @Before
  public void setUp() throws Exception {
    mapperBean = new ReMapTestBeansMapperBean().initialize();
  }

  /**
   * Teste den Mapper.
   */
  @Test
  public void testDomainToXmlTableMapper() throws Exception {
    ReMapTestBeansMapperBean reMapTestBeansMapperBean = (ReMapTestBeansMapperBean) mapperBean;

    // @formatter:off
    AssertMapping.of(reMapTestBeansMapperBean.domainToXmlTableMapper)
      .expectReplace(DomainTable::getDate, XmlTable::getDate)
        .andTest(reMapTestBeansMapperBean.dateToCalendar())
      .ensure();
    // @formatter:on
  }

  /**
   * Teste den Mapper.
   */
  @Test
  public void testXmlToDomainTableMapper() throws Exception {
    ReMapTestBeansMapperBean reMapTestBeansMapperBean = (ReMapTestBeansMapperBean) mapperBean;

    // @formatter:off
    AssertMapping.of(reMapTestBeansMapperBean.xmlToDomainTableMapper)
      .expectReplace(XmlTable::getDate, DomainTable::getDate)
        .andTest(reMapTestBeansMapperBean.calendarToDate())
      .ensure();
    // @formatter:on
  }

}

Das Fazit

In der folgenden Tabelle sind die Durchschnittswerte für ein Mapping mit vollständig gefüllter Objekthierarchie in Millisekunden angegeben. Die Veränderungen zur vorherigen Messung sind Fett markiert.

Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,01130,105
JMapper Framework
v1.6.1.CR2 vom 14.12.2016
0,00610,078
MapStruct
v1.4.1.Final vom 11.10.2020
0,01300,113
Orika
v1.5.4 vom 20.02.2019
0,25720,442
Selma
v1.0 vom 01.05.2017
0,39130,489
reMap
v4.2.5 vom 31.07.2020
3,11770,346
Dozer
v5.5.1 vom 22.04.2014
4,10550,347
ModelMapper
v2.3.9 vom 20.11.2020
5,66160,543

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 11 (64 Bit, AdoptOpenJDK Hotspot)
  • Linux Ubuntu 64 Bit 20.04.1 LTS
  • Desktop-Rechner
  • Speicher 64 GB
  • Prozessor AMD® Ryzen 9 3950x 16-core processor × 32
  • Grafik NVIDIA Corporation TU104 [GeForce RTX 2070 SUPER]
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,00850,093
JMapper Framework
v1.6.1.CR2 vom 14.12.2016
0,00660,083
MapStruct
v1.4.1.Final vom 11.10.2020
0,01440,121
Orika
v1.5.4 vom 20.02.2019
0,23860,431
Selma
v1.0 vom 01.05.2017
0,51180,505
reMap
v4.2.5 vom 31.07.2020
3,71050,584
Dozer
v5.5.1 vom 22.04.2014
4,99840,371
ModelMapper
v2.3.9 vom 20.11.2020
5,18500,531

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, AdoptOpenJDK Hotspot)
  • Linux Ubuntu 64 Bit 20.04.1 LTS
  • Desktop-Rechner
  • Speicher 64 GB
  • Prozessor AMD® Ryzen 9 3950x 16-core processor × 32
  • Grafik NVIDIA Corporation TU104 [GeForce RTX 2070 SUPER]
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,00930,098
JMapper Framework
v1.6.1.CR2 vom 14.12.2016
0,00720,085
MapStruct
v1.3.1.Final vom 29.09.2020
0,01330,117
Orika
v1.5.4 vom 20.02.2019
0,24350,434
Selma
v1.0 vom 01.05.2017
0,50400,509
reMap
v4.2.5 vom 31.07.2020
3,72070,618
Dozer
v5.5.1 vom 22.04.2014
5,04280,416
ModelMapper
v2.3.8 vom 05.06.2020
5,11710,479

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, AdoptOpenJDK Hotspot)
  • Linux Ubuntu 64 Bit 20.04.1 LTS
  • Desktop-Rechner
  • Speicher 64 GB
  • Prozessor AMD® Ryzen 9 3950x 16-core processor × 32
  • Grafik NVIDIA Corporation TU104 [GeForce RTX 2070 SUPER]
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0260,158
JMapper Framework
v1.6.1.CR2 vom 14.12.2016
0,0150,121
MapStruct
v1.3.1.Final vom 29.09.2020
0,0510,221
Orika
v1.5.4 vom 20.02.2019
0,5020,507
Selma
v1.0 vom 01.05.2017
0,6370,508
reMap
v4.2.5 vom 31.07.2020
7,5440,822
ModelMapper
v2.3.8 vom 05.06.2020
10,7561,078
Dozer
v5.5.1 vom 22.04.2014
12,1611,431

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, AdoptOpenJDK Hotspot)
  • Linux Ubuntu 64 Bit 18.04.4 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0210,150
JMapper Framework
v1.6.1.CR2 vom 14.12.2016
0,0250,160
MapStruct
v1.3.0.Final vom 10.02.2019
0,0620,248
Orika
v1.5.4 vom 20.02.2019
0,5290,534
Selma
v1.0 vom 01.05.2017
0,7920,459
reMap
v4.1.5 vom 06.02.2019
2,9680,609
Dozer
v5.5.1 vom 22.04.2014
12,0261,282
ModelMapper
v2.3.2 vom 26.11.2018
36,1582,507

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, AdoptOpenJDK Hotspot)
  • Linux Ubuntu 64 Bit 18.04.4 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0210,156
JMapper Framework
v1.6.1.CR2 vom 14.12.2016
0,0260,162
MapStruct
v1.2.0.Final vom 17.10.2017
0,2290,423
Orika
v1.5.2 vom 06.10.2017
0,5480,530
Selma
v1.0 vom 01.05.2017
0,7830,466
reMap
v4.1.1 vom 08.11.2018
3,0690,591
Dozer
v5.5.1 vom 22.04.2014
11,9531,288
ModelMapper
v2.3.2 vom 26.11.2018
41,2522,821

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, OpenJDK)
  • Linux Ubuntu 64 Bit 18.04.1 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Keine Änderungen an den Mappern
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0180,136
JMapper Framework (v1.6.1.CR2)0,0190,137
MapStruct (v1.2.0.CR2)0,2070,409
Orika (v1.5.1)0,5240,519
Selma (v1.0)0,7880,467
reMap (v1.0.3)3,1930,531
ModelMapper (v1.1.0)7,0551,031
Dozer (v5.5.1)11,6841,193

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, OpenJDK)
  • Linux Ubuntu 64 Bit 18.04.1 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0180,134
JMapper Framework (v1.6.1.CR2)0,0190,138
MapStruct (v1.2.0.CR2)0,1810,390
Orika (v1.5.1)0,5150,538
Selma (v1.0)0,7860,470
Neu: reMap (v1.0.3)2,9400,649
ModelMapper (v1.1.0)7,1021,114
Dozer (v5.5.1)11,5182,255

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, ORACLE)
  • Linux Ubuntu 64 Bit 14.04.05 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0170,134
JMapper Framework (v1.6.1.CR2)0,0160,129
MapStruct (v1.2.0.Beta2)0,1790,393
Orika (v1.5.0)0,5360,527
Neu: Selma (v0.15)0,7760,454
ModelMapper (v0.7.8)6,9301,113
Dozer (v5.5.1)11,3151,244

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, ORACLE)
  • Linux Ubuntu 64 Bit 14.04.05 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0160,133
Neu: JMapper Framework (v1.6.0.1)0,0150,123
MapStruct (v1.0.0.Final)0,1730,388
Orika (v1.4.6)0,5310,557
ModelMapper (v0.7.5)5,6240,964
Dozer (v5.5.1)11,1381,325

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, ORACLE)
  • Linux Ubuntu 64 Bit 14.04.04 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0230,163
MapStruct (v1.0.0.Final)0,1610,368
Orika (v1.4.6)0,5340,526
Neu: ModelMapper (v0.7.5)5,6240,956
Dozer (v5.5.1)11,2201,137

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, ORACLE)
  • Linux Ubuntu 64 Bit 14.04.04 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0160,144
MapStruct (v1.0.0.Beta4)0,1520,372
Orika (v1.4.5)0,5140,534
Dozer (v5.5.1)10,3711,152

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 8 (64 Bit, ORACLE)
  • Linux Ubuntu 64 Bit 14.04.01 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Arithmetischer MittelwertStandardabweichung
ByHand (v1.0)0,0190,147
MapStruct (v1.0.0.Beta2)0,2850,458
Orika (v1.4.5)0,5290,547
Dozer (v5.5.1)10,2921,146

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 7 (64 Bit, OpenJDK)
  • Linux Ubuntu 64 Bit 14.04 LTS
  • Desktop-Rechner
  • Speicher 12 GB
  • Prozessor Intel Core i7 CPU 975 @ 3,33 GHz * 8
Mittelwert
ByHand (v1.0)0,071
MapStruct (v1.0.0.Beta2)0,853
Orika (v1.4.5)2,328
Dozer (v5.5.1)46,259

Unter den folgenden Rahmenbedingungen wurde dieser Test durchgeführt.

  • Java Version 6 (32 Bit, Oracle)
  • Windows 7 Enterprise 64 Bit ServicePack 1
  • VDI (virtueller Desktop)
  • Speicher 4 GB
  • Prozessor Intel Xeon CPU ES-2670 @ 2.60 GHz 2.59 GHz

 
Alle Tests, mit den unterschiedlichen Rahmenbedingungen, sind im GitHub-Verzeichnis zu finden.

Excel Arbeitsmappe: Das zweite Tabellenblatt enthält die Messwerte. Auf dem ersten Tabellenblatt sind die Messwerte des Einschwingvorgangs.

Die Umfragen

Wer ist Ihr bevorzugter Java Bean Mapper?

  • MapStruct (28%, 11 Stimmen)
  • Dozer (21%, 8 Stimmen)
  • ByHand (15%, 6 Stimmen)
  • JMapper (15%, 6 Stimmen)
  • ModelMapper (10%, 4 Stimmen)
  • Orika (5%, 2 Stimmen)
  • Spring Framework BeanUtils (3%, 1 Stimmen)
  • Selma (3%, 1 Stimmen)
  • Apache Commons BeanUtils (0%, 0 Stimmen)

Teilnehmerzahl: 39 (1 Stimmen)

Loading ... Loading ...

Die Umfrage wurde am 31.07.2017 beendet. Diesmal gab es einen eindeutigen Sieger:
MapStruct mit 11 Stimmen vor Dozer mit 8 Stimmen. Herzlichen Glückwunsch!

Wie schon geschrieben, verwende ich vor allem MapStruct. Wie ist das bei Euch?

Wer ist Ihr bevorzugter Java Bean Mapper?

  • Dozer (33%, 3 Stimmen)
  • Orika (33%, 3 Stimmen)
  • MapStruct (33%, 3 Stimmen)
  • Spring Framework BeanUtils (0%, 0 Stimmen)
  • Apache Commons BeanUtils (0%, 0 Stimmen)
  • ByHand (0%, 0 Stimmen)

Teilnehmerzahl: 9 (1 Stimmen)

Loading ... Loading ...

Die Umfrage wurde am 19.10.2015 beendet. Diese Umfrage ging unentschieden zwischen Dozer, Orika und MapStruct mit jeweils 3 Stimmen aus.

 
Der Quellcode und Download

Quellcode ansehen bei GitHub:
Java Bean Mapper

Download einer ZIP-Datei von GitHub:
Java Bean Mapper

Die Maven Befehle

Eclipse Konfiguration neu erzeugen: $ mvn eclipse:clean eclipse:eclipse

Es wird das Eclipse Plug-in M2Eclipse verwendet.

Die Anwendungen bauen: $ mvn clean install

Frank Rahn
Letzte Artikel von Frank Rahn (Alle anzeigen)
33 Kommentare
  1. Denny
    Denny sagte:

    Hallo,

    vielen Dank für diesen ausführlichen Vergleich.
    Ist euch vielleicht ein Framework bekannt, welches ähnlich dem EqualsBuilder von Apache Commons funktioniert, jedoch 2 Objekte unterschiedlicher Typen miteinander vergleichen kann? Quasi als Prüfung ob ein Mapper korrekt funktioniert hat und alle Felder berücksichtigt hat.

    Antworten
  2. C. Schütte
    C. Schütte sagte:

    Hallo, vielen Dank für diesen ausführlichen Artikel!

    Eine weitere interessante Mapping Library ist ReMap (https://github.com/remondis-it/remap). Dieser Mapper unterstützt als einziger einen komfortablen Weg, ein Objekt-Mapping zu testen. Für Regressionstests ist dies ein wichtiges Feature. Des Weiteren vermeidet dieser Mapper, das Code angepasst werden muss (wie z.B. bei Lösungen mit Annotations).

    Vielleicht wäre diese Bibliothek auch ein guter Kandidat für diesen Artikel 🙂

    Antworten
  3. Wolfgang Häfelinger
    Wolfgang Häfelinger sagte:

    Wie sieht es mit zwei weiteren typischen Problemen aus?
    a) No Contructor: Beispiel XMLGregorianCalendar LocalDate
    b) „Coded Enum“, i.e. enum ANREDE { HERR(„01“), FRAU(„02“), UNBEKANNT(„03); … } String
    Wie gut können die Mapper-Frameworks damit umgehen? Wie aufwändig sind „Custom Converter“ zu integrieren?

    Antworten
    • Frank Rahn
      Frank Rahn sagte:

      Beispiele für Custom Converter sind in den folgenden Packages implementiert:

      • de.rahn.performance.beanmapper.vendors
      • de.rahn.performance.beanmapper.selma
        Für den Mapper Selma die Converter DateCustomMapper und CustomCalendar.
      • de.rahn.performance.beanmapper.mapstruct
        Mapper JMapper
        In der Klasse JMapperTestBeansMapperBean ab Zeile 44.

        Bei den Mapper MapStruct kann anstatt das Mapper Interface auch eine abstrakte Klasse verwendet werden, die dann Methode zur Konvertierung implementiert.

      Bei den modernen Mappern sind die Unterstützungen recht ausgereift.

      Antworten
  4. schlegel11
    schlegel11 sagte:

    Interessantes Thema.
    Ich selbst, habe erst vor kurzem, Mappings von einer XML Datei, auf verschiedene Entitäten durchführen müssen.
    Was ich dabei immer schwierig finde, ist quasi einen Adapter zu schreiben, der sowohl das Mappen, als auch die Validierung übernehmen kann. Gerade wenn man nicht auf einen DOM Parser setzen kann.
    Dabei komme ich meistens nicht drum herum, eine Implementierung von Hand vor zu nehmen.
    Eventuell müsste man sich mal für solche Fälle einen eigenen Generator schreiben.

    Antworten
    • Frank Rahn
      Frank Rahn sagte:

      Falls es für die XML ein XSD Schema gibt, kann ein JAXB Generator verwendet werden. Mittlerweile sind diese Konverter ganz gut. Für eine komplexe Validierung kann die Bean Validation JSR-303 oder die Validatoren von Spring verwendet werden. Hibernate (org.hibernate:hibernate-validator:5.3.5.Final) hat da eine gute JSR-303 v1.1 Implementierung.

      Antworten
      • schlegel11
        schlegel11 sagte:

        Genau das wäre ein guter weg gewesen. Leider existiert kein XSD Schema bzw. liegt nicht weiter vor.
        Des Weiteren ist die XML recht groß bzw. wusste ich nicht genau ob JAXB eine SAX oder DOM Implementierung nutzt, wobei hier die DOM Variante zu langsam gewesen wäre.
        Jetzt benutze ich hier pro Entität einen Adapter, welcher quasi etwas unkonventionell die Methoden dieser nochmals abbildet bzw. im Grunde die setter Methoden. Die einzelnen XML Attribute, werden dann als String Objekte übergeben und von der jeweiligen Methode validiert und in den spezifischen Typ umgewandelt bzw. in den, welcher von der Entität erwartet wird.
        Das ist leider etwas mehr Aufwand, wobei der Vorteil darin liegt überall direkt eingreifen zu können. Aber ja JAXB wäre in dem Fall einfacher gewesen.

        Antworten
        • Frank Rahn
          Frank Rahn sagte:

          Vielleicht ist ein Blick auf die Streaming API for XML (JSR-173) hilfreich oder interessant.

          Die StAX-API bietet den Zugriff auf die XML-Daten über einen Cursor an XMLStreamReader, der auf eine Stelle (Element) im Dokument zeigt und durch die Anwendung mit (next()) weiter bewegt wird. Alternativ kann auch mit einem Iterator-Verfahren XMLEventReader gearbeitet werden. Durch den Pull-Ansatz sollte die Implementierung etwas lesbarer werden – ggf. kann dieses mit JAXB kombiniert werden.

          Antworten
  5. Urs Stuber
    Urs Stuber sagte:

    Vielen Dank für Ihre Zusammenstellung!

    Welche Mapper würden Sie aktuell einer Firma guten Gewissens empfehlen im Hinblick auf die weitere Pflege? Mich irritiert beim Blick ins gitHub, dass sowohl bei Orika wie bei Dozer die meiste Entwicklung im Jahr 2013 stattfand und in letzter Zeit kaum noch Weiterentwicklungen / Bugfixes zu finden sind und keine neue Releases mehr herauskamen. Denken Sie, dass diese Mapper weiterhin gewartet werden oder ist zu befürchten, dass wenn man jetzt grösseren Entwicklungsaufwand in die Erstellung von mappings mit diesen Tools steckt, man in einer Sackgasse landen könnte („never ride a dead horse“)?

    Antworten
    • Frank Rahn
      Frank Rahn sagte:

      Ich würde aktuell entweder das JMapper Framework oder MapStruct verwenden.

      Bei MapStruct wird es demnächst die Version 1.1.0 geben – diese ist im Moment im Beta-Test.

      Der ModelMapper wird zur Zeit mäßig entwickelt. Bei den anderen Mappern sieht es düster aus. Wobei bei Dozer aber auch ein sehr hoher Reifegrad erreicht wurde. Der Dozer ist aber auch der häufigste genutzte und älteste Mapper.

      Antworten
      • Alessandro Vurro
        Alessandro Vurro sagte:

        thank you for adding JMapper between the java bean mappers.
        However, you can switch destination and source positions without rewrite the configuration, obviously if the relationship is the same:

        ...
        	@PostConstruct
        	public void initialize() {
        		JMapperAPI jMapperAPI = new JMapperAPI()
        			.add(mappedClass(...)
        				.add(conversion("dateToCalendar").from("date").to("date")
        					.body("java.util.Calendar c = java.util.Calendar.getInstance(); c.setTime(${source}); return c;")))
        				.add(conversion("calendarToDate").from("date").to("date")
        					.body("return ${source}.getTime();"))
        			.add(mappedClass(...));
        		domainToXmlMapper = new JMapper(XmlTable.class, DomainTable.class, jMapperAPI);
        		xmlToDomainMapper = new JMapper(DomainTable.class, XmlTable.class, jMapperAPI);
        	}
        ...

        If you want to avoid creating multiple instances of JMapper you should use RelationalJMapper.

        Antworten
          • Ralf Treuherz
            Ralf Treuherz sagte:

            1) Die Aussage „Dieser Mapper ähnelt den schon vorher beschriebenen Mappern Orika und Dozer und sortiert sich auch entsprechend mit der Performanz ein.“ bezüglich ModelMapper scheint den Messergebnissen nicht zu entsprechen ( 0,531 ms für Orika vs. 5,624 ms für ModelMapper)
            2) Eigene Tests mit komplexen Objektstrukturen unterstützen die Performanz von ModelMapper, hier habe ich um die 0.5 ms gemessen.
            3) Eine Messung mit Ihrer Testsuite habe ich selber nicht vorgenommen, Wenn diese tatsächlich einen Performance-Unterschied den Faktor 10 von Orika gegenüber ModelMapper zeigt, ist die Aussage in 1) zumindest irreführend.

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Ihre E-Mail-Adresse wird nicht veröffentlicht. Ihr Kommentar wird verschlüsselt an meinen Server gesendet. Erforderliche Felder sind mit * markiert.

Weitere Informationen und Widerrufshinweise finden Sie in meiner Datenschutzerklärung.