Spring Boot 2 Actual Warfare: Integrating MapStruct Type Conversion Artifact

Keywords: Java Spring Attribute Lombok

1. pain spot

The emergence of a framework to solve a pain point, I think the following inconvenient operation is often written.
Suppose the Car class is a database mapping class:
​​

 package cn.felord.mapstruct.entity;
 
 import lombok.Data;
 
 /**
  * Car
  *
  * @author Felordcn
  * @since 13:35 2019/10/12
  **/
 @Data
 public class Car {
     private String make;
     private int numberOfSeats;
     private CarType type;
 
 }

CarType Class:

 package cn.felord.mapstruct.entity;
 
 import lombok.Data;
 
 /**
  * CarType
  *
  * @author Felordcn
  * @since 13:36 2019/10/12
  **/
 @Data
 public class CarType {
     private String type;
 }

​​
CarDTO is a DTO class:

 package cn.felord.mapstruct.entity;
 
 import lombok.Data;
 
 /**
  * CarDTO
  *
  * @author Felordcn
  * @since 13:37 2019/10/12
  **/
 @Data
 public class CarDTO {
     private String make;
     private int seatCount;
     private String type;
 } 

​​
We query Car from the database and then need to convert it to CarDTO. Usually we write a method to do this:

     public CarDTO carToCarDTO(Car car) {
         CarDTO carDTO = new CarDTO();
         
         carDTO.setMake(car.getMake());
         carDTO.setSeatCount(car.getNumberOfSeats());
         carDTO.setType(car.getCarType().getType());
         // Probably longer 
         return carDTO;
     } 

This style of writing is very tedious and tasteless, and has no technical content. Even in the middle, there are many complicated operations such as type conversion, nesting and so on, and all we want is to establish the mapping relationship between them. Is there a common mapping tool to help us do this? Of course, there are many. Some people say that apache's BeanUtil.copyProperties can be implemented, but performance is poor and exceptional, and many specifications strictly prohibit the use of this approach. The following is a comparison of several object mapping frameworks. MapStruct has the highest performance in most cases. The principle is similar to lombok, MapStruct is implemented at compile time, and based on Getter and Setter, no reflection is used, so there is generally no runtime performance problem.
​​

Today, we will start mapstructure and integrate it with Spring Boot 2.x. Both idea and eclipse recommend installing the MapStruct Plugin plug-in, but it's also possible not to install it.

2. Spring Boot 2.1.9 Integrated MapStruct

Introduce MapStruct's maven-dependent coordinates under Spring Boot's pom.xml:

         <dependencies>
            <dependency>
                 <groupId>org.mapstruct</groupId>
                 <artifactId>mapstruct</artifactId>
                 <version>${mapstruct.version}</version>
                 <scope>compile</scope>
             </dependency>
             <dependency>
                 <groupId>org.mapstruct</groupId>
                 <artifactId>mapstruct-processor</artifactId>
                 <version>${mapstruct.version}</version>
                 <scope>compile</scope>
             </dependency>
                   <!-- other dependencies -->
         </dependencies>

​​

3. Use MapStruct

Let's fix the pain in the beginning. See how MapStruct can reduce your programming costs.

3.1 Write mapping from source to target

Write Car to CarDTO mapping:

 package cn.felord.mapstruct.mapping;
 
 import cn.felord.mapstruct.entity.Car;
 import cn.felord.mapstruct.entity.CarDTO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 
 /**
  * CarMapping
  *
  * @author Felordcn
  * @since 14 :02 2019/10/12
  */
 @Mapper
 public interface CarMapping {
     /**
      * Use injection Spring not to write in actual development of invoking instances
      */
     CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);
 
 
     /**
      *  The source type, the target type, the member variables, the same type and the same variable name need not be mapped by {@link org.mapstruct.Mapping}.
      *
      * @param car the car
      * @return the car dto
      */
     @Mapping(target = "type", source = "carType.type")
     @Mapping(target = "seatCount", source = "numberOfSeats")
     CarDTO carToCarDTO(Car car);
 
 }

3.2 Explanation of MapStruct Mapping Method

Just a few lines of code can be very simple! Explain the procedure:

First, it is declared that a mapping interface is marked with @org.mapstruct.Mapper (not confused with the mybatis annotation), indicating that it is an entity type conversion interface. Here we declare a CAR_MAPPING to facilitate our call. Is CarDTO to CarDTO (Car car) familiar with abstracting our transformation methods like mybatis? @ The org.mapstruct.Mapping annotation is used to declare the mapping of member attributes. The annotation has two important attributes:

  • Source represents the source of the transformation. This is Car.
  • Target represents the target of the transformation. This is CarDTO.

This is based on the parameter name of the member variable. If there is a member variable of carType type nested in Car, for example, its type attribute maps the type string in CarDTO, we use type.type to get the attribute value. If there are multiple layers and so on. MapStruct ultimately calls setter and getter methods, not reflection. This is also one of the reasons for its better performance. numberOfSeats mapping to seatCount is easier to understand. Are we forgetting an attribute make, because their locations and names are identical, so they can be omitted? And for the packaging class, it is automatically unpacked and sealed, and thread-safe. MapStruct not only has these functions, but also has other complex functions:

Set conversion defaults and constants. When the target value is null, we can set its default value. Note that these are basic types and corresponding boxing types, as follows

 @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")

Note that constants cannot reference sources (they cannot specify source attributes). The following is the correct operation:

 @Mapping(target = "stringConstant", constant = "Constant Value")

3.2 Mapper Compilation

When your application is compiled. You will find that an implementation class is generated in a compiled directory such as maven, which is a subdirectory under target generated-sources annotations. For example, CarMapping above will generate CarMapping Impl as follows:

 package cn.felord.mapstruct.mapping;
  
 import cn.felord.mapstruct.entity.Car;
 import cn.felord.mapstruct.entity.CarDTO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 import javax.annotation.Generated;
 import org.springframework.stereotype.Component;
 
 @Generated(
     value = "org.mapstruct.ap.MappingProcessor",
     date = "2019-10-12T15:05:36+0800",
     comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_222 (Amazon.com Inc.)"
 )
 @Component
 public class CarMappingImpl implements CarMapping {
 
     @Override
     public CarDTO carToCarDTO(Car car) {
         if ( car == null ) {
             return null;
         }
 
         CarDTO carDTO = new CarDTO();
 
         carDTO.setType( carCarTypeType( car ) );
         carDTO.setSeatCount( car.getNumberOfSeats() );
         carDTO.setMake( car.getMake() );
 
         return carDTO;
     }
 
     private String carCarTypeType(Car car) {
         if ( car == null ) {
             return null;
         }
         CarType carType = car.getCarType();
         if ( carType == null ) {
             return null;
         }
         String type = carType.getType();
         if ( type == null ) {
             return null;
         }
         return type;
     }
 }

4. MapStruct Advanced Operations

Here are several advanced operations of MapStruct:

4.1 Formatting Operation

Formatting is also an operation we often use, such as digital formatting and date formatting.
This is a digital formatting operation, following the specifications of java.text.DecimalFormat:

     @Mapping(source = "price", numberFormat = "$#.00")

The following shows the formatting operation of mapping a date set to a date string set:

 @IterableMapping(dateFormat = "dd.MM.yyyy")
 List<String> stringListToDateList(List<Date> dates);

4.2 Use java expressions

The following demonstrates how to inject the addTime attribute as the current time value using LocalDateTime.

First, import LocalDateTime into the imports attribute of @org.mapstruct.Mapper, which is an array, meaning that you can import more processing classes as needed:

 @Mapper(imports = {LocalDateTime.class})

Next, just add the annotation @org.mapstruct.Mapping to the corresponding method, whose attribute expression receives an expression included in java():

  • No entry version:

     @Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
  • With the reference version, we inject the Car factory date string manufactureDateStr into CarDTO's Local DateTime type attribute addTime:

     @Mapping(target = "addTime", expression = "java(LocalDateTime.parse(car.manufactureDateStr))")
     CarDTO carToCarDTO(Car car);

4.3 MapStruct Conversion Mapper Injection into Spring IoC Container

If we want to inject Mapper into the Spring IoC container, we just need to declare that instead of building a single instance, we can reference CarMapping as other spring bean s do:

  package cn.felord.mapstruct.mapping;
  
  import cn.felord.mapstruct.entity.Car;
  import cn.felord.mapstruct.entity.CarDTO;
  import org.mapstruct.Mapper;
  import org.mapstruct.Mapping;
  import org.mapstruct.factory.Mappers;
 
 /**
  * CarMapping Injection spring Writing
  *
  * @author Felordcn
  * @since 14 :02 2019/10/12
  */
 @Mapper(componentModel = "spring")
 public interface CarMapping {
     /**
      * Use injection Spring not to write in actual development of invoking instances
      */
 //    CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);
 
 
     /**
      * Source type, target type, member variable, same type, same variable name, mapping without writing {@link Mapping}
      *
      * @param car the car
      * @return the car dto
      */
     @Mapping(target = "type", source = "carType.type")
     @Mapping(target = "seatCount", source = "numberOfSeats")
     CarDTO carToCarDTO(Car car);
 
 }

​​

5. summary

In fact, MapStruct has many other functions. But in terms of readability, I recommend using the above several easy-to-understand functions. If you are interested, you can go to mapstruct.org for further study. With lombok and the jsr303 I introduced, you can focus more on your business and code more clearly.

Focus on Public Number: Felordcn Gets More Information

Personal blog: https://felord.cn

Posted by rhodrykorb on Sat, 12 Oct 2019 01:30:09 -0700