Performances des cadres de mappage Java

Performances des cadres de cartographie Java

1. Introduction

La création d'applications Java de grande taille composées de plusieurs couches nécessite l'utilisation de plusieurs modèles, tels que le modèle de persistance, le modèle de domaine ou ce que l'on appelle les DTO. L'utilisation de plusieurs modèles pour différentes couches d'application nous obligera à fournir un moyen de mappage entre les haricots.

Faire cela manuellement peut rapidement créer beaucoup de code passe-partout et prendre beaucoup de temps. Heureusement pour nous, il existe plusieurs frameworks de mappage d'objets pour Java.

Dans ce didacticiel, nous allons comparer les performances des frameworks de mappage Java les plus populaires.

2. Cartographie des cadres

2.1. Dozer

Dozer is a mapping framework that uses recursion to copy data from one object to another. La structure peut non seulement copier les propriétés entre les beans, mais également convertir automatiquement entre différents types.

Pour utiliser le framework Dozer, nous devons ajouter cette dépendance à notre projet:


    net.sf.dozer
    dozer
    5.5.1

Plus d'informations sur l'utilisation du framework Dozer peuvent être trouvées dans cearticle.

La documentation du framework peut être trouvéehere.

2.2. Orika

Orika is a bean to bean mapping framework that recursively copies data from one object to another.

Le principe général de travail de l'Orika est similaire à celui de Dozer. La principale différence entre les deux est le fait queOrika uses bytecode generation. Cela permet de générer des mappeurs plus rapides avec une surcharge minimale.

Pour l'utiliser, we doit ajouter une telle dépendance à notre projet:


    ma.glasnost.orika
    orika-core
    1.5.2

Des informations plus détaillées sur l'utilisation de l'Orika peuvent être trouvées dans cearticle.

La documentation actuelle du framework peut être trouvéehere.

2.3. MapStruct

MapStruct est un générateur de code qui génère automatiquement des classes de mappage de bean.

MapStruct a également la capacité de convertir entre différents types de données. Vous trouverez plus d'informations sur son utilisation dans cearticle.

Pour ajouter MapStruct à notre projet, nous devons inclure la dépendance suivante:

3
    org.mapstruct
    mapstruct-processor
    1.2.0.Final

La documentation du framework peut être trouvéehere.

2.4. ModelMapper

ModelMapper est un framework qui vise à simplifier le mappage d'objets, en déterminant comment les objets se mappent les uns aux autres en se basant sur des conventions. Il fournit une API sécurisée pour le type et la refactorisation.

Plus d'informations sur le framework peuvent être trouvées dans lesdocumentation.

Pour inclure le ModelMapper à notre projet, nous devons ajouter la dépendance suivante:


  org.modelmapper
  modelmapper
  1.1.0

2.5. JMapper

JMapper est le cadre de mappage qui vise à fournir un mappage facile à utiliser et hautes performances entre Java Beans.

Le cadre vise à appliquer le principe DRY à l'aide d'annotations et de cartographie relationnelle.

La structure permet différents modes de configuration: basée sur les annotations, XML ou sur les API.

Plus d'informations sur le framework peuvent être trouvées dans sesdocumentation.

Pour inclure le JMapper dans notre projet, nous devons ajouter sa dépendance:


    com.googlecode.jmapper-framework
    jmapper-core
    1.6.0.1

3. Essai Modèle

Pour pouvoir tester correctement la cartographie, nous avons besoin de modèles source et cible. Nous avons créé deux modèles de test.

Le premier est juste un simple POJO avec un champString, cela nous a permis de comparer des frameworks dans des cas plus simples et de vérifier si quelque chose change si nous utilisons des beans plus compliqués.

Le modèle source simple ressemble à ce qui suit:

public class SourceCode {
    String code;
    // getter and setter
}

Et sa destination est assez similaire:

public class DestinationCode {
    String code;
    // getter and setter
}

L'exemple réel de source bean ressemble à ceci:

public class SourceOrder {
    private String orderFinishDate;
    private PaymentType paymentType;
    private Discount discount;
    private DeliveryData deliveryData;
    private User orderingUser;
    private List orderedProducts;
    private Shop offeringShop;
    private int orderId;
    private OrderStatus status;
    private LocalDate orderDate;
    // standard getters and setters
}

Et la classe cible ressemble à celle ci-dessous:

public class Order {
    private User orderingUser;
    private List orderedProducts;
    private OrderStatus orderStatus;
    private LocalDate orderDate;
    private LocalDate orderFinishDate;
    private PaymentType paymentType;
    private Discount discount;
    private int shopId;
    private DeliveryData deliveryData;
    private Shop offeringShop;
    // standard getters and setters
}

La structure entière du modèle peut être trouvéehere.

4. Convertisseurs

Pour simplifier la conception de la configuration de test, nous avons créé l'interfaceConverter qui ressemble à ci-dessous:

public interface Converter {
    Order convert(SourceOrder sourceOrder);
    DestinationCode convert(SourceCode sourceCode);
}

Et tous nos mappeurs personnalisés implémenteront cette interface.

4.1. OrikaConverter

Orika permet une implémentation complète de l'API, cela simplifie grandement la création du mappeur:

public class OrikaConverter implements Converter{
    private MapperFacade mapperFacade;

    public OrikaConverter() {
        MapperFactory mapperFactory = new DefaultMapperFactory
          .Builder().build();

        mapperFactory.classMap(Order.class, SourceOrder.class)
          .field("orderStatus", "status").byDefault().register();
        mapperFacade = mapperFactory.getMapperFacade();
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return mapperFacade.map(sourceOrder, Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return mapperFacade.map(sourceCode, DestinationCode.class);
    }
}

4.2. DozerConverter

Dozer nécessite un fichier de mappage XML, avec les sections suivantes:



    
        com.example.performancetests.model.source.SourceOrder
        com.example.performancetests.model.destination.Order
        
            status
            orderStatus
        
    
    
        com.example.performancetests.model.source.SourceCode
        com.example.performancetests.model.destination.DestinationCode
    

Après avoir défini le mappage XML, nous pouvons l’utiliser à partir du code:

public class DozerConverter implements Converter {
    private final Mapper mapper;

    public DozerConverter() {
        DozerBeanMapper mapper = new DozerBeanMapper();
        mapper.addMapping(
          DozerConverter.class.getResourceAsStream("/dozer-mapping.xml"));
        this.mapper = mapper;
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return mapper.map(sourceOrder,Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return mapper.map(sourceCode, DestinationCode.class);
    }
}

4.3. MapStructConverter

La définition de la structure de la carte est assez simple car elle repose entièrement sur la génération de code:

@Mapper
public interface MapStructConverter extends Converter {
    MapStructConverter MAPPER = Mappers.getMapper(MapStructConverter.class);

    @Mapping(source = "status", target = "orderStatus")
    @Override
    Order convert(SourceOrder sourceOrder);

    @Override
    DestinationCode convert(SourceCode sourceCode);
}

4.4. JMapperConverter

JMapperConverter nécessite plus de travail. Après avoir implémenté l'interface:

public class JMapperConverter implements Converter {
    JMapper realLifeMapper;
    JMapper simpleMapper;

    public JMapperConverter() {
        JMapperAPI api = new JMapperAPI()
          .add(JMapperAPI.mappedClass(Order.class));
        realLifeMapper = new JMapper(Order.class, SourceOrder.class, api);
        JMapperAPI simpleApi = new JMapperAPI()
          .add(JMapperAPI.mappedClass(DestinationCode.class));
        simpleMapper = new JMapper(
          DestinationCode.class, SourceCode.class, simpleApi);
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return (Order) realLifeMapper.getDestination(sourceOrder);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return (DestinationCode) simpleMapper.getDestination(sourceCode);
    }
}

Nous devons également ajouter des annotations@JMap à chaque champ de la classe cible. De plus, JMapper ne peut pas effectuer de conversion entre les types d'énumération à lui seul et nous oblige à créer des fonctions de mappage personnalisées:

@JMapConversion(from = "paymentType", to = "paymentType")
public PaymentType conversion(com.example.performancetests.model.source.PaymentType type) {
    PaymentType paymentType = null;
    switch(type) {
        case CARD:
            paymentType = PaymentType.CARD;
            break;

        case CASH:
            paymentType = PaymentType.CASH;
            break;

        case TRANSFER:
            paymentType = PaymentType.TRANSFER;
            break;
    }
    return paymentType;
}

4.5. ModelMapperConverter

ModelMapperConverter ne nécessite que de fournir les classes que nous voulons mapper:

public class ModelMapperConverter implements Converter {
    private ModelMapper modelMapper;

    public ModelMapperConverter() {
        modelMapper = new ModelMapper();
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
       return modelMapper.map(sourceOrder, Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return modelMapper.map(sourceCode, DestinationCode.class);
    }
}

5. Test de modèle simple

Pour les tests de performances, nous pouvons utiliser Java Microbenchmark Harness, plus d'informations sur la façon de l'utiliser peuvent être trouvées dans cearticle.

Nous avons créé un benchmark distinct pour chaqueConverter en spécifiantBenchmarkMode toMode.All.

5.1. AverageTime

JMH a renvoyé les résultats suivants pour la durée moyenne (le moins c'est le mieux):

image

Ce repère indique clairement que MapStruct et JMapper ont les meilleurs temps de travail moyens.

5.2. Throughput

Dans ce mode, le repère renvoie le nombre d'opérations par seconde. Nous avons reçu les résultats suivants (more is better):

image

En mode débit, MapStruct était le plus rapide des frameworks testés, suivi de près par JMapper.

5.3. SingleShotTime

Ce mode permet de mesurer le temps d'une seule opération du début à la fin. Le repère a donné le résultat suivant (moins c'est mieux):

image

Nous voyons ici que JMapper renvoie un résultat nettement meilleur que MapStruct.

5.4. SampleTime

Ce mode permet d'échantillonner le temps de chaque opération. Les résultats pour trois centiles différents sont les suivants:

image

Tous les benchmarks ont montré que MapStruct et JMapper sont tous deux de bons choix en fonction du scénario, bien que MapStruct ait donné des résultats significativement moins bons pourSingleShotTime.

6. Tests de modèles réels

Pour les tests de performances, nous pouvons utiliser Java Microbenchmark Harness, plus d'informations sur la façon de l'utiliser peuvent être trouvées dans cearticle.

Nous avons créé un benchmark séparé pour chaqueConverter en spécifiantBenchmarkMode toMode.All.

6.1. AverageTime

JMH a renvoyé les résultats suivants pour la durée moyenne (moins c'est mieux):

image

6.2. Throughput

Dans ce mode, benchmark renvoie le nombre d'opérations par seconde. Pour chacun des mappeurs, nous avons obtenu les résultats suivants (plus c'est mieux):

image

6.3. SingleShotTime

Ce mode permet de mesurer le temps d'une seule opération du début à la fin. La référence a donné les résultats suivants (moins c'est mieux):

image

6.4. SampleTime

Ce mode permet d'échantillonner le temps de chaque opération. Les résultats d'échantillonnage sont divisés en centiles, nous présenterons les résultats pour trois centiles différents p0.90, p0.999, et p1.00:

image

Bien que les résultats exacts de l'exemple simple et de l'exemple réel soient clairement différents, ils suivent la même tendance. Les deux exemples ont donné des résultats similaires en termes d'algorithme le plus rapide et d'algorithme le plus lent.

6.5. Conclusion

Sur la base des tests de modèles réels que nous avons effectués dans cette section, nous pouvons constater que la meilleure performance appartient clairement à MapStruct. Dans les mêmes tests, nous constatons que Dozer se situe systématiquement au bas de notre tableau de résultats.

7. Summary

Dans cet article, nous avons effectué des tests de performances de cinq frameworks de mappage de bean Java populaires: ModelMapper, MapStruct, Orika, Dozer et JMapper.

Comme toujours, des échantillons de code peuvent être trouvésover on GitHub.