Desempenho das estruturas de mapeamento Java

Desempenho das estruturas de mapeamento Java

1. Introduction

A criação de grandes aplicativos Java compostos por várias camadas requer o uso de vários modelos, como modelo de persistência, modelo de domínio ou os chamados DTOs. O uso de vários modelos para diferentes camadas de aplicativos exigirá uma maneira de mapear entre beans.

Fazer isso manualmente pode criar rapidamente muito código padrão e consumir muito tempo. Felizmente para nós, existem várias estruturas de mapeamento de objetos para Java.

Neste tutorial, vamos comparar o desempenho das estruturas de mapeamento Java mais populares.

2. Mapping Frameworks

2.1. Dozer

Dozer is a mapping framework that uses recursion to copy data from one object to another. A estrutura pode não apenas copiar propriedades entre os beans, mas também pode converter automaticamente entre tipos diferentes.

Para usar a estrutura Dozer, precisamos adicionar essa dependência ao nosso projeto:


    net.sf.dozer
    dozer
    5.5.1

Mais informações sobre o uso da estrutura Dozer podem ser encontradas nestearticle.

A documentação do framework pode ser encontradahere.

2.2. Orika

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

O princípio geral do trabalho do Orika é semelhante ao Dozer. A principal diferença entre os dois é o fato deOrika uses bytecode generation. Isso permite gerar mapeadores mais rápidos com a sobrecarga mínima.

Para usá-lo, we precisa adicionar essa dependência ao nosso projeto:


    ma.glasnost.orika
    orika-core
    1.5.2

Informações mais detalhadas sobre o uso do Orika podem ser encontradas nestearticle.

A documentação real do framework pode ser encontradahere.

2.3. MapStruct

MapStruct é um gerador de código que gera classes de mapeador de bean automaticamente.

O MapStruct também tem a capacidade de converter entre diferentes tipos de dados. Mais informações sobre como usá-lo podem ser encontradas nestearticle.

Para adicionar MapStruct ao nosso projeto, precisamos incluir a seguinte dependência:

3
    org.mapstruct
    mapstruct-processor
    1.2.0.Final

A documentação do framework pode ser encontradahere.

2.4. ModelMapper

O ModelMapper é um framework que visa simplificar o mapeamento de objetos, determinando como os objetos são mapeados entre si com base em convenções. Ele fornece API de tipo seguro e refatoração segura.

Mais informações sobre a estrutura podem ser encontradas emdocumentation.

Para incluir o ModelMapper em nosso projeto, precisamos adicionar a seguinte dependência:


  org.modelmapper
  modelmapper
  1.1.0

2.5. JMapper

O JMapper é a estrutura de mapeamento que visa fornecer um mapeamento de alto desempenho fácil de usar entre o Java Beans.

O framework visa aplicar o princípio DRY usando Anotações e mapeamento relacional.

A estrutura permite diferentes formas de configuração: baseada em anotações, XML ou API.

Mais informações sobre o framework podem ser encontradas em seudocumentation.

Para incluir o JMapper em nosso projeto, precisamos adicionar sua dependência:


    com.googlecode.jmapper-framework
    jmapper-core
    1.6.0.1

3. Teste Modelo

Para poder testar o mapeamento corretamente, precisamos ter modelos de origem e destino. Criamos dois modelos de teste.

O primeiro é apenas um POJO simples com um campoString, o que nos permitiu comparar frameworks em casos mais simples e verificar se algo muda se usarmos beans mais complicados.

O modelo de origem simples é semelhante a seguir:

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

E seu destino é bastante semelhante:

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

O exemplo da vida real do bean de origem é assim:

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
}

E a classe de destino se parece abaixo:

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
}

Toda a estrutura do modelo pode ser encontradahere.

4. Conversores

Para simplificar o design da configuração de teste, criamos a interfaceConverter que se parece com a seguinte:

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

E todos os nossos mapeadores personalizados implementarão essa interface.

4.1. OrikaConverter

O Orika permite a implementação completa da API, isso simplifica bastante a criação do mapeador:

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

O Dozer requer um arquivo de mapeamento XML, com as seguintes seções:



    
        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
    

Após definir o mapeamento XML, podemos usá-lo no código:

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

A definição de estrutura de mapa é bastante simples, pois se baseia inteiramente na geração de código:

@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 requer mais trabalho a ser feito. Depois de implementar a 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);
    }
}

Também precisamos adicionar@JMap annotations a cada campo da classe de destino. Além disso, o JMapper não pode converter entre tipos de enum por conta própria e exige que criemos funções de mapeamento personalizadas:

@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 requer apenas o fornecimento das classes que queremos mapear:

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. Teste de modelo simples

Para o teste de desempenho, podemos usar Java Microbenchmark Harness, mais informações sobre como usá-lo podem ser encontradas nestearticle.

Criamos um benchmark separado para cadaConverter especificandoBenchmarkMode toMode.All.

5.1. AverageTime

JMH retornou os seguintes resultados para o tempo médio de execução (quanto menor, melhor):

image

Esta referência mostra claramente que o MapStruct e o JMapper têm os melhores tempos médios de trabalho.

5.2. Throughput

Nesse modo, o benchmark retorna o número de operações por segundo. Recebemos os seguintes resultados (more is better):

image

No modo de taxa de transferência, o MapStruct foi o mais rápido das estruturas testadas, com o JMapper em segundo.

5.3. SingleShotTime

Este modo permite medir o tempo de operação única do início ao fim. O benchmark deu o seguinte resultado (menos é melhor):

image

Aqui, vemos que o JMapper retorna um resultado significativamente melhor que o MapStruct.

5.4. SampleTime

Este modo permite amostrar o tempo de cada operação. Os resultados para três percentis diferentes são os seguintes:

image

Todos os benchmarks mostraram que MapStruct e JMapper são boas escolhas dependendo do cenário, embora MapStruct tenha dado resultados significativamente piores paraSingleShotTime.

6. Teste de modelo da vida real

Para o teste de desempenho, podemos usar Java Microbenchmark Harness, mais informações sobre como usá-lo podem ser encontradas nestearticle.

Criamos um benchmark separado para cadaConverter especificandoBenchmarkMode toMode.All.

6.1. AverageTime

JMH retornou os seguintes resultados para o tempo médio de execução (menos é melhor):

image

6.2. Throughput

Nesse modo, o benchmark retorna o número de operações por segundo. Para cada um dos mapeadores, recebemos os seguintes resultados (quanto mais, melhor):

image

6.3. SingleShotTime

Este modo permite medir o tempo de operação única do início ao fim. O benchmark deu os seguintes resultados (menos é melhor):

image

6.4. SampleTime

Este modo permite amostrar o tempo de cada operação. Os resultados da amostragem são divididos em percentis, apresentaremos resultados para três percentis diferentes p0,90, p0,999,e p1,00:

image

Embora os resultados exatos do exemplo simples e do exemplo da vida real sejam claramente diferentes, eles seguem a mesma tendência. Ambos os exemplos deram resultados semelhantes em termos de qual algoritmo é o mais rápido e qual é o mais lento.

6.5. Conclusão

Com base nos testes de modelo da vida real que realizamos nesta seção, podemos ver que o melhor desempenho pertence claramente ao MapStruct. Nos mesmos testes, vemos que o Dozer está consistentemente na parte inferior da nossa tabela de resultados.

7. Summary

Neste artigo, conduzimos testes de desempenho de cinco estruturas de mapeamento de bean Java populares: ModelMapper, MapStruct, Orika, Dozer e JMapper.

Como sempre, as amostras de código podem ser encontradasover on GitHub.