头图

Preface

In our daily development of hierarchical applications, in order to decouple each layer from each other, different objects are generally defined to transfer data between different layers. Therefore, there are various XXXDTO , XXXVO , XXXBO For objects derived from database objects, when transferring data between different layers, it is inevitable that these objects often need to be converted to each other.

At this time, there are generally two processing methods: ① directly use the Setter and Getter methods to convert, ② use some tools for conversion (eg BeanUtil.copyProperties ). In the first way, if there are many object attributes, you need to write a lot of code Getter/Setter Although the second method seems to be much simpler than the first method, because it uses reflection, the performance is not very good, and there are many pitfalls in use. MapStruct to be introduced today solves the shortcomings of these two methods without affecting performance.

What is MapStruct

MapStruct is a code generator, which is based on the convention over the configuration method, which greatly simplifies the implementation of the mapping between Java bean The automatically generated mapping conversion code only uses simple method calls, so it is fast, type-safe and easy to understand and read. The address of the Github MapStruct . In general, there are three characteristics as follows:

  1. Annotation-based
  2. Automatically generate mapping conversion code at compile time
  3. Type safety, high performance, no dependencies

MapStruct use steps

MapStruct is relatively simple, only the following three steps are required.

① Introduce dependency (here we take Gradle as an example)

dependencies {
    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}

② Create related conversion objects

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Doctor {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class DoctorDTO {

  private Integer id;

  private String name;
}

③ Create a converter class (Mapper)

It should be noted that the converter does not necessarily have to Mapper , but the official example recommends XXXMapper Here is the simplest mapping case (the field name and type are exactly matched), just need to @Mapper add 06110852955118 annotation to the converter class, the converter code is as follows:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface DoctorMapper {

  DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);

  DoctorDTO toDTO(Doctor doctor);
}

The following simple test is used to verify whether the conversion result is correct. The test code is as follows:

/**
 * @author mghio
 * @since 2021-08-08
 */
public class DoctorTest {

  @Test
  public void testToDTO() {
    Integer doctorId = 9527;
    String doctorName = "mghio";

    Doctor doctor = new Doctor();
    doctor.setId(doctorId);
    doctor.setName(doctorName);

    DoctorDTO doctorDTO = DoctorMapper.INSTANCE.toDTO(doctor);

    assertEquals(doctorId, doctorDTO.getId());
    assertEquals(doctorName, doctorDTO.getName());
  }

}

The test results passed normally, indicating that the use of the DoctorMapper converter has achieved our expected results.

1.png

Analysis of MapStruct Implementation

In the above example, using MapStruct to achieve the Doctor to DoctorDTO in three simple steps, then, MapStruct do it? In fact, through the converter we defined, we can find that the converter is of the interface type, and we know that in Java , the interface cannot provide functions, but the specification is defined, and the specific work is still its implementation class.

Therefore, we can boldly guess that MapStruct must have generated an implementation class for our defined converter interface ( DoctorMapper Mappers.getMapper(DoctorMapper.class) actually obtained the implementation class of the converter interface. The following is verified debug in the test class:

2.png

debug can be seen from DoctorMapper.INSTANCE obtains the interface implementation class DoctorMapperImpl . This converter interface implementation class is automatically generated compile time Gradle project is under build/generated/sources/anotationProcessor/Java (the Maven project is under the target/generated-sources/annotations directory). The implementation class source code for the above example converter interface is generated as follows:

4.png

It can be found that the automatically generated code is similar to what we usually write by hand. It is simple and easy to understand. The code is generated during compilation and has no runtime dependencies. Compared with the implementation using reflection, there is another advantage that it is easy to go to debug implement the source code to locate when an error occurs, while the reflection is relatively more difficult to locate.

Introduction to common usage scenarios

① The object attribute name and type are exactly the same

As can be seen from the above example, when the attribute name and type are exactly the same, we only need to define a converter interface and add the @Mapper annotation, and then MapStruct will automatically generate the implementation class to complete the conversion. The sample code is as follows:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  Target toTarget(Source source);
}

② The object attribute type is the same but the name is different

When the object attribute type is the same but the attribute name is different, you can manually specify the conversion @Mapping The sample code is as follows:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;

  private String sourceName;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;

  private String targetName;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);
}

③ Use custom conversion method in Mapper

Sometimes, for certain types (for example: a class whose attributes are custom classes), it cannot be processed in the form of automatically generated code. At this point we need methods to customize the type of conversion, in JDK 7 previous version, you need to use an abstract class to define the conversion Mapper , and in JDK 8 can use the interface above version default method from the method defined type conversion. The sample code is as follows:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;

  private String sourceName;

  private InnerSource innerSource;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class InnerSource {

  private Integer deleted;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;

  private String targetName;

  private InnerTarget innerTarget;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class InnerTarget {

  private Boolean isDeleted;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);

  default InnerTarget innerTarget2InnerSource(InnerSource innerSource) {
    if (innerSource == null) {
      return null;
    }
    InnerTarget innerTarget = new InnerTarget();
    innerTarget.setIsDeleted(innerSource.getDeleted() == 1);
    innerTarget.setName(innerSource.getName());
    return innerTarget;
  }
}

④ Convert multiple objects into one object and return

In the process of some actual business coding, it is inevitable that multiple objects need to be converted into one object. MapStruct can also support it well. For this kind of final return information from multiple classes, we can achieve it through configuration Many-to-one conversion. The sample code is as follows:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Doctor {

  private Integer id;

  private String name;
}
/**
 * @author mghio
 * @since 2021-08-09
 */
@Data
public class Address {

  private String street;

  private Integer zipCode;
}
/**
 * @author mghio
 * @since 2021-08-09
 */
@Mapper
public interface AddressMapper {

  AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

  @Mapping(source = "doctor.id", target = "personId")
  @Mapping(source = "address.street", target = "streetDesc")
  DeliveryAddressDTO doctorAndAddress2DeliveryAddressDTO(Doctor doctor, Address address);
}

From the converter ( AddressMapper ) in this example, it can be seen that when the attribute name and type exactly match, it can also be automatically converted, but when the source object has multiple attribute names and types that are exactly the same as the target object, you still need to manually configure and specify MapStruct , because 0611085295560a cannot accurately determine which attribute conversion should be used at this time.

Several ways to obtain the converter (Mapper)

The way to obtain the converter is different according to the @Mapper annotation of componentModel , and supports the following four different values:

  1. default default method, the default method, use the factory method ( Mappers.getMapper(Class) ) to get
  2. cdi The mapper generated at this time is CDI bean scope of the application, use the @Inject annotation to get
  3. spring Spring can be obtained through the @Autowired annotation, it is recommended to use this method Spring
  4. jsr330 generated maps Used @javax.inject.Named and @Singleton notes, by @Inject to get

① Obtained through factory

In the example above are obtained by way of the plant, is used MapStruct provided Mappers.getMapper(Class<T> clazz) method to obtain the specified type Mapper . Then when calling, there is no need to create objects repeatedly. The final implementation of the method is to load MapStruct through the class loader that defines the interface (the class name rule is: interface name + Impl ), and then call the class of no Create the object with the constructor. The core source code is as follows:

5.png

② Use dependency injection to obtain

For dependency injection ( dependency injection ), Spring framework for development should be familiar with it, and it is often used in work. MapStruct also supports the use of dependency injection, and the official recommendation is to use dependency injection to obtain it. Use Spring dependent manner only need to specify injection @Mapper annotations componentModel = "spring" to, the following sample code:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper(componentModel = "spring")
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);

}  

The reason we can use @Autowired SourceMapper the implementation class of the 061108529558af interface has been registered as a Bean in the container. You can also see the code of the interface implementation class generated as follows, and the @Component annotation is automatically added to the class.

6.png

Finally, there are two considerations: ① When two attributes convert object is inconsistent (for example, DoctorDTO does not exist Doctor objects in a field), there will be warning when compiling. You can configure ignore = true @Mapping annotation, or when there are many inconsistent fields, you can directly set the unmappedTargetPolicy attribute of the @Mapper unmappedSourcePolicy attribute to ReportingPolicy.IGNORE . Lombok is also used in your project, please note that the version of Lombok 1.18.10 or above, otherwise the compilation will fail. I also stepped on this pit when I first started using it. . .

Summarize

This article introduces the object conversion tool Mapstruct library to reduce our conversion code in a safe and elegant way. As can be seen from the examples in the article, Mapstruct provides a large number of functions and configurations, allowing us to create simple to complex mappers in a simple and quick way. What is introduced in the article is only Mapstruct library, and there are many powerful functions that are not mentioned in the article. Interested friends can check the official user guide .


mghio
446 声望870 粉丝