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):
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):
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):
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:
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):
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):
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):
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:
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.