“Zefengengchen” wechat technology account is now open, in order to get the first-hand technical articles push, welcome to search attention!

At the beginning of the article, let’s first answer the question left by the previous article, namely:

Why design multiple Entities?

The layered architecture based on the principle of “separation of concerns” is often used in application architecture design, such as the well-known MVC/MVP/MVVM architecture design pattern, divided into the presentation layer, business logic layer, data access layer, persistence layer and so on. In order to maintain the independence of application architecture after layered, it is usually necessary to define different data models between each layer, so it is inevitable to face the problem of data model transformation.

Common data models at different levels include:

VO(View Object) : View Object, used for display layer, associated with a specific page display data.

Data Transfer Object (DTO) : Used for the transport layer, refers to the Data that is transmitted and interacted with the server.

Domain Object (DO) : Domain Object used in the business layer to perform data required by specific business logic.

PO(Persistent Object) : A Persistent Object used in the persistence layer to persist data stored locally.

Again, take messaging in instant messaging as an example:

  • After the client edits the message on the session page and sends it, the data related to the message is constructed as MessageVO in the presentation layer and displayed in the chat records on the session page.
  • After the presentation layer transforms MessageVO into the corresponding MessagePO of the persistence layer, it calls the persistence method of the persistence layer to save the message to the local database or elsewhere
  • After the presentation layer transforms MessageVO into the MessageDTO required by the transport layer, the transport layer transfers the data to the server
  • As for the corresponding reverse operation, I believe you can also come out for the reasoning, here is no longer repeated.

In the previous article, we manually wrote the mapping code as a GET /set operation, which was cumbersome and error-prone, and after some research to improve development efficiency, considering that the same thing would have to be repeated later when extending other message types, We decided to use the MapStruct library to do this for us in an automated way.

So what’s a MapStruct?

MapStruct is a code generator for generating type-safe, high-performance, non-dependent mapping code.

All we need to do is to define a Mapper interface and declare the mapping method we need to implement. We can use MapStruct annotation processor at compile time to generate the implementation class of the interface. The implementation class helps us to complete get/set operations in an automatic way to realize the mapping relationship between the source object and the target object.

The use of MapStruct

Add MapStruct dependencies in Gradle form:

Add the following to the build.gradle file at the module level:

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

If the project is using the Kotlin language:

dependencies { ... Implementation "org. Mapstruct: mapstruct: 1.4.2. The Final" kapt (" org. Mapstruct: mapstruct - processor: 1.4.2. The Final ")}Copy the code

Next, we will use MapStruct to automate the field mapping between MessageVO and MessageDTO as the operation objects defined above:

Create the mapper interface

  1. Create a Java interface (also in the form of an abstract class) and add the @mapper annotation to indicate that it is a Mapper:
  2. Declare a mapping method specifying the input and output parameter types:
@Mapper
public interface MessageEntityMapper {
    
    MessageDTO.Message.Builder vo2Dto(MessageVO messageVo);

    MessageVO dto2Vo(MessageDTO.Message messageDto);
    
}
Copy the code

Used here MessageDTO. Message. Builder rather than MessageDTO. The cause of the Message is that ProtoBuf generated Message using the Builder pattern, and in order to prevent external direct instantiation and the structural parameter is set to private, This is going to cause the MapStruct to report an error at compile time, and you’ll see why when you’re done.

Implicit mapping in the default scenario

When the field name of the incoming parameter type is the same as the field name of the outgoing parameter type, MapStruct helps us implicitly map, that is, we don’t need to actively process it.

The following types of automatic conversions are currently supported:

  • Basic data types and their wrapper types
  • Between numeric types, but converting from a large data type to a smaller data type (for example, from long to int) can result in a loss of accuracy
  • Between basic data types and strings
  • Enumeration between types and strings
  • .

This is actually the idea of convention over configuration:

Convention over Configuration, also known as programming by convention, is a software design paradigm designed to reduce the number of decisions a software developer has to make, with the benefit of simplicity without losing flexibility.

Essentially, the developer only needs to specify the non-conforming parts of the application. If the conventions of the tools you’re using match your expectations, you can dispense with configuration; Instead, you can configure it to work the way you want.

In MapStruct libraries, we just need to configure the processing for those fields that MapStruct libraries can’t do implicit mapping for us.

For example, MessageVO and MessageDTO have the same names and data types as messageId, senderId, targetId and TIMESTAMP, so we don’t need to deal with them.

Field mapping in special scenarios

Field names are inconsistent:

In this case, simply add the @mapping annotation on top of the Mapping method, annotating the name of the source field and the name of the target field.

ProtoBuf generates messagedto. Message for messageTypeValue. ProtoBuf generates messagedto. Message for messageTypeValue. We can use the above method to complete the mapping from messageType to messageTypeValue:

    @Mapping(source = "messageType", target = "messageTypeValue")
    MessageDTO.Message.Builder vo2Dto(MessageVO messageVo);
Copy the code
Inconsistent field types:

In this case, you only need to declare an additional mapping method for the two different data types: one that takes the type of the source field as the incoming parameter type and the type of the target field as the outgoing parameter type.

The MapStruct checks to see if the mapping method exists, and if so, it calls it in the implementation class of the mapper interface to complete the mapping.

For example, in our example, the content field is defined as bytes. In the generated messagedto. Message class, the content field is defined as String. Therefore, we need to declare an additional byte2String mapping method and a string2Byte mapping method in the mapper interface:

    default String byte2String(ByteString byteString) {
        return new String(byteString.toByteArray());
    }

    default ByteString string2Byte(String string) {
        return ByteString.copyFrom(string.getBytes());
    }
Copy the code

For example, if we want to map messageType to enumeration type instead of messageType to messageTypeValue, we can declare the following two mapping methods:

    default int enum2Int(MessageDTO.Message.MessageType type) {
        return type.getNumber();
    }

    default String byte2String(ByteString byteString) {
        return new String(byteString.toByteArray());
    }
Copy the code
Ignore certain fields:

For special needs, some new fields may be added to the data model of some levels to process specific businesses. These fields have no meaning for other levels, so it is not necessary to retain these fields in other levels. At the same time, in order to avoid errors caused by the failure to find corresponding fields in MapStruct implicit mapping, Ignore = true to ignore these fields:

For example, ProtoBuf generates messagedto. Message with three additional fields, mergeFrom, senderIdBytes, and targetIdBytes, which are not necessary for MessageVO. So we need to get MapStruct to ignore it for us:

    @Mapping(target = "mergeFrom", ignore = true)
    @Mapping(target = "senderIdBytes", ignore = true)
    @Mapping(target = "targetIdBytes", ignore = true)
    MessageDTO.Message.Builder vo2Dto(MessageVO messageVo);
Copy the code

Additional processing for other scenarios

Front, as we have said, because the MessageDTO. The constructor of Message is set to private to compile times wrong, actually MessageDTO. Message. The Builder of the constructor is private, The Builder is instantiated by MessageDTO. Message. NewBuilder () method.

And MapStruct by default calls the default constructor of the target class to do the mapping, so there’s nothing we can do about it?

In fact, MapStruct allows you to customize object factories that will provide factory methods that you can call to get an instance of the target type.

All we need to do is declare the return type of the factory method as our target type, return an instance of that target type in the factory method the way we want, and pass in our factory class by adding the use parameter to the @mapper annotation of the Mapper interface. The MapStruct automatically finds the factory method first, and instantiates the target type.

public class MessageDTOFactory { public MessageDTO.Message.Builder createMessageDto() { return MessageDTO.Message.newBuilder(); }}Copy the code
@Mapper(uses = MessageDTOFactory.class)
public interface MessageEntityMapper {
Copy the code

Finally, we define a member named INSTANCE that returns a singleton of the mapper interface type by calling the Mappers.getMapper() method and passing in the mapper interface type.

public interface MessageEntityMapper {

    MessageEntityMapper INSTANCE = Mappers.getMapper(MessageEntityMapper.class);
Copy the code

The complete mapper interface code is as follows:

@Mapper(uses = MessageDTOFactory.class) public interface MessageEntityMapper { MessageEntityMapper INSTANCE = Mappers.getMapper(MessageEntityMapper.class); @Mapping(source = "messageType", target = "messageTypeValue") @Mapping(target = "mergeFrom", ignore = true) @Mapping(target = "senderIdBytes", ignore = true) @Mapping(target = "targetIdBytes", ignore = true) MessageDTO.Message.Builder vo2Dto(MessageVO messageVo); MessageVO dto2Vo(MessageDTO.Message messageDto); @Mapping(source = "messageTypeValue", target = "messageType") default MessageDTO.Message.MessageType int2Enum(int value) { return MessageDTO.Message.MessageType.forNumber(value); } default int enum2Int(MessageDTO.Message.MessageType type) { return type.getNumber(); } default String byte2String(ByteString byteString) { return new String(byteString.toByteArray()); } default ByteString string2Byte(String string) { return ByteString.copyFrom(string.getBytes()); }}Copy the code

Automatically generate an implementation class for the mapper interface

Mapping interface definition, when we rebuild project MapStruct will help us to generate the interface implementation class, we can in {module} / build/generated/source/kapt/debug / {package} find the class path, to explore the details:

public class MessageEntityMapperImpl implements MessageEntityMapper { private final MessageDTOFactory messageDTOFactory = new MessageDTOFactory(); @Override public Builder vo2Dto(MessageVO messageVo) { if ( messageVo == null ) { return null; } Builder builder = messageDTOFactory.createMessageDto(); if ( messageVo.getMessageType() ! = null ) { builder.setMessageTypeValue( messageVo.getMessageType() ); } if ( messageVo.getMessageId() ! = null ) { builder.setMessageId( messageVo.getMessageId() ); } if ( messageVo.getMessageType() ! = null ) { builder.setMessageType( int2Enum( messageVo.getMessageType().intValue() ) ); } builder.setSenderId( messageVo.getSenderId() ); builder.setTargetId( messageVo.getTargetId() ); if ( messageVo.getTimestamp() ! = null ) { builder.setTimestamp( messageVo.getTimestamp() ); } builder.setContent( string2Byte( messageVo.getContent() ) ); return builder; } @Override public MessageVO dto2Vo(Message messageDto) { if ( messageDto == null ) { return null; } MessageVO messageVO = new MessageVO(); messageVO.setMessageId( messageDto.getMessageId() ); messageVO.setMessageType( enum2Int( messageDto.getMessageType() ) ); messageVO.setSenderId( messageDto.getSenderId() ); messageVO.setTargetId( messageDto.getTargetId() ); messageVO.setTimestamp( messageDto.getTimestamp() ); messageVO.setContent( byte2String( messageDto.getContent() ) ); return messageVO; }}Copy the code

You can see, as mentioned above, that since the implementation class actually does the field mapping with normal GET /set method calls, no reflection is used, and because the class is generated at compile time, reducing the runtime performance cost, it meets its definition of “high performance.”

On the other hand, when the attribute mapping error, can be timely informed at compile time, avoid the runtime error crash, and for some specific types of non-empty judgment measures, so it conforms to the definition of “type safety”.

Next, we can replace the manually written mapping code with the mapping method for this mapper instance:

Class EnvelopeHelper {companion object {/** * fill (VO->DTO) */ fun stuff(envelope: Envelope): MessageDTO.Message? { return envelope.messageVO? .run { MessageEntityMapper.INSTANCE.vo2Dto(this).build() } ?: /** * extract(messageDTO: messageDTO.Message) */ extract(messageDTO: messageDTO. Envelope? { with(Envelope()) { messageVO = MessageEntityMapper.INSTANCE.dto2Vo(messageDTO) return this } } } }Copy the code

conclusion

As you can see, the end result is that we reduce a lot of boilerplate code, make the overall structure of the code easier to understand, and later extend other types of objects by adding mapping methods, which also improves the readability/maintainability/extensibility of the code.

MapStruct follows the principle of convention over configuration, and helps us solve the tedious and error-prone inter-conversion work between different data models in the application hierarchical architecture in as automatic a way as possible. It is really a powerful tool to greatly improve the development efficiency of developers!

“Zefengengchen” wechat technology account is now open, in order to get the first-hand technical articles push, welcome to search attention!