Analysis of Java object transformation scheme and practice of mapstruct

Keywords: Java Back-end

I. Preface

With the continuous refinement of system module layering, the conversion of various objects is inevitably involved in the daily development of Java, such as DO, DTO, VO, etc. writing mapping conversion code is a cumbersome, repetitive and error prone work. With the help of a good tool, it not only reduces the workload, improves the development efficiency, but also reduces the occurrence of bug s.

II. Common schemes and analysis

1 fastjson

CarDTO entity = JSON.parseObject(JSON.toJSONString(carDO), CarDTO.class);

The performance of this scheme is very poor because the intermediate json format string is generated and then converted into the target object. At the same time, because the intermediate json format string will be generated, if too many conversions are made, gc will be very frequent. At the same time, the support ability for complex scenes is insufficient, so it is basically rarely used.

2. Beanutil class

BeanUtil.copyProperties() combines handwritten get and set. For simple transformations, BeanUtil is used directly. For complex transformations, get and set are written manually. The pain point of this scheme is that the coding efficiency is low, the redundancy is complicated and slightly ugly, and the performance of BeanUtil is not high because it uses reflection invoke to assign values.

It can only be used in scenarios where the number of bean s is small, the content is small, and the conversion is not frequent.

apache.BeanUtils

org.apache.commons.beanutils.BeanUtils.copyProperties(do, entity);

This scheme has poor performance due to the use of reflection and its own design problems. The group development statute clearly stipulates that it is prohibited to use.

spring.BeanUtils

org.springframework.beans.BeanUtils.copyProperties(do, entity);

This scheme has made many optimizations for apache's BeanUtils, and the overall performance has been improved a lot. However, the reflection implementation is still not as good as the native code processing. Secondly, it has insufficient support for complex scenes.

3 beanCopier

BeanCopier copier = BeanCopier.create(CarDO.class, CarDTO.class, false); 
copier.copy(do, dto, null);

This scheme dynamically generates a subclass of a proxy class, which is actually converted into the best performance get and set methods through bytecode. The important overhead is to create a BeanCopier. The overall performance is close to the native code processing, which is much better than BeanUtils, especially when there is a large amount of data, but the support ability for complex scenes is insufficient.

4. Various Mapping frameworks

classification

From a broad perspective, Object Mapping technology is divided into two types: run-time conversion and compile time conversion:

  • Runtime reflection calls set/get or directly assigns values to member variables. In this way, the assignment is performed through invoke, and the implementation will generally use beautil, javassist and other open-source libraries. The representatives of runtime object transformation are Dozer and ModelMaper.
  • The class file of set/get code is generated dynamically during compilation, and the set/get method of the class is called directly at run time. In this way, there will still be set/get code, but developers do not need to write it themselves. Such representatives are mapstruct, Selma and orika.

analysis

  • No matter what kind of Mapping framework, it is basically configured by the user in the form of xml configuration file or annotation, and then the Mapping relationship is generated.
  • The method of generating class files during compilation requires that DTO still has set/get methods, but the calls are shielded; In the run-time reflection mode, in some schemes that directly fill in the field, the set/get code can also be omitted.
  • The method of generating class during compilation will the source code locally, which is convenient for troubleshooting.
  • The method of generating class at compile time. Because java and class files only appear at compile time, hot deployment will be affected to some extent.
  • Because many contents of reflective type are black boxes, it is not as convenient as generating class during compilation when troubleshooting problems. Refer to the Java object mapper benchmark project on GitHub to see the performance comparison of the main frameworks.
  • Because reflective calls are executed according to the mapping relationship at run time, the execution speed will be significantly reduced by N orders of magnitude.
  • The method of generating class code during compilation is not different from writing code directly. However, because the code is generated by template, the code quality is not as high as that written manually, which will also cause a certain performance loss.

Comprehensive performance, maturity, ease of use and scalability, mapstruct is an excellent framework.

III. Mapstruct User Guide

1 Maven introduction

2 simple introduction cases

DO and DTO

lombok simplified code is used here. The principle of lombok is to generate simplified code such as get and set during compilation.

@Data 
public class Car {     
    private String make;     
    private int numberOfSeats;     
    private CarType type; 
}
@Data 
public class CarDTO {     
    private String make;     
    private int seatCount;     
    private String type; 
}

Define Mapper

@Mapper describes the mapping. When editing, mapstruct will generate implementation classes according to this description:

  • When an attribute has the same name as its target entity copy, it is implicitly mapped.
  • When the attributes in the target entity have different names, you can specify their names through the @ Mapping annotation.
@Mapper 
public interface CarMapper {     
    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); }

Using Mapper

Use Mappers factory to generate static instances.

@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);  

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}
Car car = new Car(...); 
CarDTO carDTO = CarMapper.INSTANCE.CarToCarDTO(car);

getMapper will the implementation class of Impl suffix of the load interface.

By generating spring bean injection, Mapper annotation and spring configuration will automatically generate a bean, which can be accessed directly by bean injection.

@Mapper(componentModel = "spring") 
public interface CarMapper {     
    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}

Automatically generated MapperImpl content

If spring bean access is configured, @ Component will be automatically added to the annotation.

3 Advanced use

Reverse mapping

If it is bidirectional mapping, such as from DO to DTO and from DTO to DO, the mapping rules of forward and reverse methods are usually similar, and can be simply reversed by switching the source and target.

Using annotations@
InheritInverseConfiguration indicates that the method should inherit the reverse configuration of the corresponding reverse method.

@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);    

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car);    

    @InheritInverseConfiguration     
    Car CarDTOToCar(CarDTO carDTO); 
}

Update bean mapping

In some cases, the mapping transformation is not required to generate a new bean, but to update the existing bean.

@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    void updateDTOFromCar(Car car, @MappingTarget CarDTO carDTO);

Set mapping

The mapping of collection types (List, Set, Map, etc.) is completed in the same way as the mapping bean type, that is, by defining the mapping method with the required source type and target type in the mapper interface. MapStruct supports multiple iteratable types in the Java Collection Framework.

The generated code will contain a loop that traverses the source collection, transforms each element and puts it into the target collection. If a mapping method for the collection element type is found in the given mapper or the mapper it uses, this method is called to perform the element conversion, and if there is an implicit conversion for the source element type and the target element type, this conversion is called.

@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); 

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car);     

    List<CarDTO> carsToCarDtos(List<Car> cars);     

    Set<String> integerSetToStringSet(Set<Integer> integers);     

    @MapMapping(valueDateFormat = "dd.MM.yyyy")     
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source); 
}

Implementation classes generated at compile time:

Multiple source parameter mapping

MapStruct also supports mapping methods with multiple source parameters. For example, combine multiple entities into one data transfer object.

Add a Person object in the original case, and add the driverName attribute in CarDTO, which is obtained according to the Person object.

@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "car.numberOfSeats", target = "seatCount")     
    @Mapping(source = "person.name", target = "driverName")     
    CarDTO CarToCarDTO(Car car, Person person); }

Compiled code:

Default and constant mapping

If the corresponding source property is null, you can specify a default value to set the predefined value as the target property. In any case, you can specify constants to set such predefined values. Default values and constants are specified as String values. When the target type is original type or boxed type, the String value will be literal. In this case, bit / octal / decimal / hexadecimal mode is allowed as long as they are valid text. In all other cases, constants or default values are type converted by built-in conversion or calling other mapping methods to match the type required by the target property.

@Mapper 
public interface SourceTargetMapper {     
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );     

    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")     
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")     
    @Mapping(target = "stringConstant", constant = "Constant Value")     
    @Mapping(target = "integerConstant", constant = "14")     
    @Mapping(target = "longWrapperConstant", constant = "3001")     
    @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")     
    @Mapping(target = "stringListConstants", constant = "jack-jill-tom")     
    Target sourceToTarget(Source s); 
}

Custom mapping method or mapper

In some cases, you may need to manually implement a specific mapping from one type to another that MapStruct cannot generate.

You can define the default implementation method in Mapper, and generate the conversion code to call the relevant methods:

@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    @Mapping(source = "length", target = "lengthType")     
    CarDTO CarToCarDTO(Car car);     

    default String getLengthType(int length) {         
        if (length > 5) {             
            return "large";         
        } else {             
            return "small";         
        }     
    } 
}

Other mappers can also be defined. In the following case, Date in Car needs to be converted to String in DTO:

public class DateMapper {     
    public String asString(Date date) {         
        return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;     
    }     

    public Date asDate(String date) {         
        try {             
            return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).parse( date ) : null;         
        } catch ( ParseException e ) {             
            throw new RuntimeException( e );         
        }     
    } 
}
@Mapper(uses = DateMapper.class) 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}

Compiled code:

If ambiguity occurs when multiple similar method calls are encountered, use @ qualifiedBy to specify:

@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    @Mapping(source = "length", target = "lengthType", qualifiedByName = "newStandard")     
    CarDTO CarToCarDTO(Car car);     

    @Named("oldStandard")     
    default String getLengthType(int length) {         
        if (length > 5) {             
            return "large";         
        } else {             
            return "small";         
        }     
    }     
    @Named("newStandard")     
    default String getLengthType2(int length) {         
        if (length > 7) {             
            return "large";         
        } else {             
            return "small";         
        }     
    } 
}

Expression custom mapping

Expressions can contain structures from multiple languages.

Currently only Java is supported as the language. For example, this function can be used to call constructors, and the entire source object can be used in expressions. It should be noted that only valid java code is inserted: MapStruct does not validate expressions at build time, but displays errors in classes generated during compilation.

@Data 
@AllArgsConstructor 
public class Driver {     
    private String name;     
    private int age; 
}
@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "car.numberOfSeats", target = "seatCount")     
    @Mapping(target = "driver", expression = "java( new com.alibaba.my.mapstruct.example4.beans.Driver(person.getName(), person.getAge()))")     
    CarDTO CarToCarDTO(Car car, Person person); 
} 

The default expression is a combination of the default value and the expression:

@Mapper( imports = UUID.class )
public interface SourceTargetMapper {     
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );     

    @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")     
    Target sourceToTarget(Source s); 
}

Decorator custom mapping

In some cases, you may need to customize the generated mapping method, such as setting additional properties in the target object that cannot be set by the generated method.

The implementation is also very simple. An abstract class of the Mapper is implemented in the decorator mode. The annotation @ DecoratedWith is added to the Mapper to point to the decorator class. It is still called normally when in use.

@Mapper 
@DecoratedWith(CarMapperDecorator.class) 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}
public abstract class CarMapperDecorator implements CarMapper {     
    private final CarMapper delegate;     
    protected CarMapperDecorator(CarMapper delegate) {         
        this.delegate = delegate;     
    }     
    @Override     
    public CarDTO CarToCarDTO(Car car) {         
        CarDTO dto = delegate.CarToCarDTO(car);         
        dto.setMakeInfo(car.getMake() + " " + new SimpleDateFormat( "yyyy-MM-dd" ).format(car.getCreateDate()));         
        return dto;     
    } 
}
Relevant reference

Copyright notice: the content of this article is spontaneously contributed by Alibaba cloud real name registered users. The copyright belongs to the original author. Alibaba cloud developer community does not own its copyright or bear corresponding legal liabilities. Please refer to Alibaba cloud developer community user service agreement and Alibaba cloud developer community intellectual property protection guidelines for specific rules.

Posted by Paul1893 on Tue, 30 Nov 2021 15:12:12 -0800