mapstruct best practices

The original link address of this article: http://nullpointer.pw/mapstruct%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html

Preface

According to the daily development habits, different JavaBean objects are used to transfer data for different domain layers to avoid mutual influence. Therefore, based on the database entity object User, objects such as UserDto and UserVo are derived. Therefore, when data is transferred between different layers, it is inevitable to transform these objects.

Common conversion methods include:

  • Call getter/setter method for property assignment
  • Call beanutil.copyproperty to assign reflection property

The first way, needless to say, is to write a lot of getter/setter code when there are many properties. The second method is much easier than the first one, but there are many pitfalls. For example, the source and target are written reversely, so it is difficult to locate where a field is assigned. At the same time, due to the use of reflection, the performance is also poor.

In view of this, today I will write the third method of object transformation. In this paper, I use MapStruct tool for transformation The principle is also very simple, that is to generate the corresponding assignment code in the code compilation phase. The underlying principle is to call the getter/setter method, but this is done by the tool for us. Mapstructure solves the disadvantages of the first two methods without affecting the performance, which is very good~

Preparation

In order to explain the use of mapstructure tool, this article uses common User classes and corresponding UserDto objects to demonstrate.

@Data
@Accessors(chain = true)
public class User {
    private Long id;
    private String username;
    private String password; // Password
    private Integer sex;  // Gender
    private LocalDate birthday; // Birthday
    private LocalDateTime createTime; // Creation time
    private String config; // Other extension information, stored in JSON format
    private String test; // Test field
}

@Data
@Accessors(chain = true)
public class UserVo {
    private Long id;
    private String username;
    private String password;
    private Integer gender;
    private LocalDate birthday;
    private String createTime;
    private List<UserConfig> config;
      private String test; // Test field
    @Data
    public static class UserConfig {
        private String field1;
        private Integer field2;
    }
}

Notice the difference between the two classes.

1, MapStruct configuration and basic use

The dependency of mapstructure in the project

<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
  <version>1.3.1.Final</version>
</dependency>
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct-processor</artifactId>
  <version>1.3.1.Final</version>
</dependency>

Because the object conversion operations in the project are basically the same, except for a conversion base class, different objects can directly inherit the base class if they are only simple conversion, without any method of overriding the base class, that is, only one empty class is needed. If the subclass overrides the method of the base class, @ Mapping on the base class will fail.

@MapperConfig
public interface BaseMapping<SOURCE, TARGET> {

    /**
     * Map properties with the same name
     */
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    TARGET sourceToTarget(SOURCE var1);

    /**
     * Reverse, map properties with the same name
     */
    @InheritInverseConfiguration(name = "sourceToTarget")
    SOURCE targetToSource(TARGET var1);

    /**
     * Mapping properties with the same name, set form
     */
    @InheritConfiguration(name = "sourceToTarget")
    List<TARGET> sourceToTarget(List<SOURCE> var1);

    /**
     * Reverse, map property with same name, set form
     */
    @InheritConfiguration(name = "targetToSource")
    List<SOURCE> targetToSource(List<TARGET> var1);

    /**
     * Map attribute with the same name, set flow form
     */
    List<TARGET> sourceToTarget(Stream<SOURCE> stream);

    /**
     * Reverse, map property of the same name, set flow form
     */
    List<SOURCE> targetToSource(Stream<TARGET> stream);
}

Implement the converter of User and UserVo objects

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring")
public interface UserMapping extends BaseMapping<User, UserVo> {

    @Mapping(target = "gender", source = "sex")
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Override
    UserVo sourceToTarget(User var1);

    @Mapping(target = "sex", source = "gender")
    @Mapping(target = "password", ignore = true)
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Override
    User targetToSource(UserVo var1);

    default List<UserConfig> strConfigToListUserConfig(String config) {
        return JSON.parseArray(config, UserConfig.class);
    }

    default String listUserConfigToStrConfig(List<UserConfig> list) {
        return JSON.toJSONString(list);
    }
}

The example in this article uses the method of spring. The value of the componentModel attribute of @ Mapper annotation is spring, but it should be developed in this mode that most of them use.

@Mapping is used to configure the mapping relationship of objects. In the example, the gender attribute of User object is named sex, and the gender attribute of UserVo object is named gender. Therefore, the target and source attributes need to be configured.

The password field should not be returned to the foreground. There are two ways not to convert it. The first is that the password field does not appear in vo object. The second is to set the field ignore = true in @ Mapping.

Mapstructure provides the time formatted property dataFormat, which supports the conversion of time types such as Date, LocalDate, LocalDateTime and String. In the example, the birthday property is of type LocalDate, which can automatically complete the conversion without specifying dataFormat. The default of type LocalDateTime is ISO format time, which is often not in line with the requirements in China. Therefore, you need to specify dataFormat manually.

2, Custom attribute type conversion method

Generally, mapstructure can complete the type field conversion for us, but some of them are our custom object types, so mapstructure cannot perform field conversion, which requires us to write the corresponding type conversion methods. The author uses JDK8, which supports the default methods in the interface, and can directly add the custom type conversion methods in the converter.

In the example, the config attribute of the User object is a JSON string, and the UserVo object is of type List. This requires the mutual conversion of JSON string and object.

default List<UserConfig> strConfigToListUserConfig(String config) {
  return JSON.parseArray(config, UserConfig.class);
}

default String listUserConfigToStrConfig(List<UserConfig> list) {
  return JSON.toJSONString(list);
}

If it is below JDK8, the default method is not supported. You can define another converter, and then reference it through uses = XXX.class in the current converter's @ Mapper.

After the method is defined, MapStruct will call our custom conversion method for conversion when it matches the field of the appropriate type.

3, Unit test

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class MapStructTest {

  @Resource
  private UserMapping userMapping;

  @Test
  public void tetDomain2DTO() {
    User user = new User()
      .setId(1L)
      .setUsername("zhangsan")
      .setSex(1)
      .setPassword("abc123")
      .setCreateTime(LocalDateTime.now())
      .setBirthday(LocalDate.of(1999, 9, 27))
      .setConfig("[{\"field1\":\"Test Field1\",\"field2\":500}]");
    UserVo userVo = userMapping.sourceToTarget(user);
    log.info("User: {}", user);
    //        User: User(id=1, username=zhangsan, password=abc123, sex=1, birthday=1999-09-27, createTime=2020-01-17T17:46:20.316, config=[{"field1":"Test Field1","field2":500}])
    log.info("UserVo: {}", userVo);
    //        UserVo: UserVo(id=1, username=zhangsan, gender=1, birthday=1999-09-27, createTime=2020-01-17 17:46:20, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
  }

  @Test
  public void testDTO2Domain() {
    UserConfig userConfig = new UserConfig();
    userConfig.setField1("Test Field1");
    userConfig.setField2(500);

    UserVo userVo = new UserVo()
      .setId(1L)
      .setUsername("zhangsan")
      .setGender(2)
      .setCreateTime("2020-01-18 15:32:54")
      .setBirthday(LocalDate.of(1999, 9, 27))
      .setConfig(Collections.singletonList(userConfig));
    User user = userMapping.targetToSource(userVo);
    log.info("UserVo: {}", userVo);
    //        UserVo: UserVo(id=1, username=zhangsan, gender=2, birthday=1999-09-27, createTime=2020-01-18 15:32:54, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
    log.info("User: {}", user);
    //        User: User(id=1, username=zhangsan, password=null, sex=2, birthday=1999-09-27, createTime=2020-01-18T15:32:54, config=[{"field1":"Test Field1","field2":500}])
  }

4, FAQs

  1. When the properties of the two objects are inconsistent, for example, when a field in the User object does not exist in UserVo, there will be a warning prompt when compiling. You can configure ignore = true in @ Mapping. When there are many fields, you can directly set the unmappedTargetPolicy property or the unmappedSourcePolicy property to ReportingPolicy.IGNORE in @ Mapper.
  2. If Lombok is used in the project at the same time, it must be noted that the version of Lombok is equal to or higher than 1.18.10. Otherwise, compilation fails. The author fell into this pit for a long time before climbing out. I hope you don't step on the pit again.

Code download

The code involved in this article has been uploaded to Github for reference.

Reference resources

Tags: Java JSON Attribute Spring Lombok

Posted on Sat, 21 Mar 2020 06:44:22 -0700 by BLottman