preface

In our daily development of hierarchical applications, different objects are defined to transfer data between different layers in order to decoupled each other. Therefore, there are various XXXDTO, XXXVO, XXXBO and other objects derived from database objects. When transferring data between different layers, Inevitably, these objects often need to be converted to each other.

There are two ways to do this: ① convert directly using Setter and Getter methods, and ② convert using some utility class (e.g. beanutil.copyproperties). The first method requires a lot of Getter/Setter code if the object has a lot of properties. The second method, while much simpler than the first, does not perform well because it uses reflection and has many pitfalls. MapStruct, the protagonist of today’s presentation, addresses the drawbacks of both approaches without affecting performance.

What is MapStruct

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on the convention over configuration approach. The automatically generated mapping transformation code uses only simple method calls, so it is fast, type-safe, and easy to understand and read. In general, it has the following three characteristics:

  1. Based on the annotation
  2. Automatically generate map transformation code at compile time
  3. Type safe, high performance, dependency free

MapStruct usage steps

MapStruct is easy to use in three steps.

① Introduce dependencies (here withGradleAs an example)

dependencies {
    implementation 'org. Mapstruct: mapstruct: 1.4.2. Final'
    annotationProcessor 'org. Mapstruct: mapstruct - processor: 1.4.2. The Final'
}
Copy the code

② Create related conversion objects

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

  private Integer id;

  private String name;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Data
public class DoctorDTO {

  private Integer id;

  private String name;
}
Copy the code

Create Mapper class

Note that converters don’t always end with Mapper, but the official example recommends naming converters in XXXMapper format. Here’s an example of the simplest mapping case (where the field name and type match exactly). Just add the @mapper annotation to the converter class. The converter code looks like this:

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

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

  DoctorDTO toDTO(Doctor doctor);
}
Copy the code

Verify the conversion results with a simple test like this:

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

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

    Doctor doctor = newDoctor(); doctor.setId(doctorId); doctor.setName(doctorName); DoctorDTO doctorDTO = DoctorMapper.INSTANCE.toDTO(doctor); assertEquals(doctorId, doctorDTO.getId()); assertEquals(doctorName, doctorDTO.getName()); }}Copy the code

The test results passed normally, indicating that the expected results were achieved with the DoctorMapper converter.

Analysis of MapStruct implementation

In the example above, the Doctor to DoctorDTO conversion is implemented in three simple steps using MapStruct. How does MapStruct do this? In fact, the converters defined by us can be seen that the converters are interface types, and we know that in Java, interfaces do not provide functionality, just define the specification, the concrete work is its implementation class.

So we can take a wild guess that MapStruct must have generated an implementation class for the transformer interface we defined (DoctorMapper), The converter obtained by Mappers. GetMapper (doctormapper.class) is actually the implementation class of the converter interface. Verify this by debugging in the test class:

The debug command displays that doctormapper. INSTANCE obtains the DoctorMapperImpl implementation class of the interface. This converter interface implementation class is automatically generated at compile time, Gradle project is in the build/generated/sources/anotationProcessor/Java (Maven project in target/generated – sources/annotations directory). The source code for the implementation class that generates the above sample converter interface is as follows:

As you can see, the automatically generated code is similar to what we usually write by hand. It is simple and easy to understand, and the code is completely generated at compile time with no runtime dependencies. Another advantage compared to using reflection is that it is easy to debug the implementation source code to locate errors, while reflection is much more difficult to locate problems.

This section describes common application scenarios

① The object attribute name and type are the same

As you can see from the previous example, when the attribute name and type are exactly the same, you just need to define a converter interface and add the @mapper annotation, and MapStruct automatically generates the implementation class to do the conversion. Example code is as follows:

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

  private Integer id;

  private String name;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Data
public class Target {

  private Integer id;

  private String name;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Mapper
public interface SourceMapper {

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

  Target toTarget(Source source);
}
Copy the code

② The object attributes have the same type but different names

When the object attribute type is the same but the attribute name is different, use the @Mapping annotation to manually specify the transformation. Example code is as follows:

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

  private Integer id;

  private String sourceName;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Data
public class Target {

  private Integer id;

  private String targetName;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Mapper
public interface SourceMapper {

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

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

③ Use custom transformation methods in Mapper

Sometimes, for some types (for example, a class whose attributes are custom classes), you can’t handle it in the form of automatically generated code. In JDK versions prior to JDK 7, we used abstract classes to define conversion Mapper. In JDK versions prior to JDK 8, we can customize conversion methods using the default methods of the interface. Example code is as follows:

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

  private Integer id;

  private String sourceName;

  private InnerSource innerSource;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Data
public class InnerSource {

  private Integer deleted;

  private String name;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Data
public class Target {

  private Integer id;

  private String targetName;

  private InnerTarget innerTarget;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-08 * /
@Data
public class InnerTarget {

  private Boolean isDeleted;

  private String name;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 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());
    returninnerTarget; }}Copy the code

④ Multiple objects are converted into one object and returned

In the process of some actual business coding, it is inevitable to convert multiple objects into one object, and MapStruct can also support it well. For this kind of final return information comes from multiple classes, we can implement many-to-one transformation through configuration. Example code is as follows:

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

  private Integer id;

  private String name;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 2021-08-09 * /
@Data
public class Address {

  private String street;

  private Integer zipCode;
}
Copy the code
/ * * *@author mghio
 * @sinceThe 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);
}
Copy the code

As you can see from the AddressMapper example, when the attribute name and type match exactly, it can also be automatically converted, but when the source object has multiple attribute names and types exactly the same as the target object, you still need to manually configure the specified. Because MapStruct can’t determine exactly which attribute conversion to use.

Several ways to get a converter (Mapper)

The way to obtain the converter depends on the @mapper annotation’s componentModel attribute. The following four different values are supported:

  1. defaultDefault mode, default mode, use factory mode (Mappers.getMapper(Class) ) to get
  2. cdiThe mapper generated at this point is application-wideCDI bean, the use of@InjectAnnotations to get
  3. spring SpringThe way can be through@AutowiredAnnotation to get, inSpringThis approach is recommended in the framework
  4. jsr330The generated mapper is used@javax.inject.Named@SingletonNote, by@InjectIn order to get

(1) Obtain it from the factory

In the examples above, we get the Mapper of the specified type using the Mappers. GetMapper (Class

clazz) method provided by MapStruct. Instead of repeatedly creating objects when called, the final implementation of the method is to load the implementation class (interface name + Impl) generated by MapStruct through the class loader we defined the interface, and then call the class’s no-argument constructor to create the object. The core source code is as follows:

(2) Obtain it using dependency injection

Dependency injection is familiar to those of you who use the Spring framework for development. MapStruct also supports dependency injection, and dependency injection is officially recommended. Use Spring dependency injection method only need to specify @mapper annotation componentModel = “Spring”, sample code as follows:

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

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

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

}  
Copy the code

The reason we can use @AutoWired is that the implementation class of the SourceMapper interface is already registered as a Bean in the container. As you can see from the code that implements the class using the generated interface below, the @Component annotation is automatically added to the class.

Two final considerations: (1) If the properties of two transformed objects are inconsistent (for example, DoctorDTO does not have a field in the Doctor object), a warning will be generated during compilation. You can configure ignore = true in the @mapping annotation, or if there are many inconsistent fields, You can directly set the @mapper annotation’s unmappedTargetPolicy property or unmappedSourcePolicy property to ReportingPolicy.ignore. If you are using Lombok in your project, be careful that Lombok is at least 1.18.10 or later, otherwise you will fail to compile. I stepped in this hole when I first started using it…

conclusion

This article introduces the Mapstruct library, an object transformation tool, to reduce our transformation code in a safe and elegant way. As you can see from the examples in this article, Mapstruct provides a great deal of functionality and configuration that allows you to create mapers from simple to complex in a simple and quick manner. This is just the tip of the iceberg of Mapstruct library, there are many powerful features not mentioned in this article, interested friends can check the official guide.