Warning: This article is more code, please read patiently

In real projects, we often need to convert two similar objects to each other in order to hide some sensitive data (e.g. passwords, encryption tokens, etc.) when providing data externally. The most common method is to create a new object and set the desired values into it. If there are multiple groups of objects that need to be converted this way, then there is a lot of meaningless work to do just get/set.

In this context, ModelMapper was born, which is a simple, efficient and intelligent object mapping tool. It’s very simple to use, starting with adding maven dependencies

< the dependency > < groupId > org. Modelmapper < / groupId > < artifactId > modelmapper < / artifactId > < version > 2.1.1 < / version > </dependency>Copy the code

You can then simply new a ModelMapper object and call its map method to map the value of the specified object to another object.

The method of use is not introduced too much today, you can Google, find the relevant documents of ModelMapper to learn. Today I’m going to share a pit I accidentally stepped on a few days ago. I have two classes, PostDO and PostVO (this is only a partial field truncation, so the meaning of the two classes is not explained) :

public class PostDO {
    private Long id;
    private String commentId;
    private Long postId;
    private int likeNum;
}
Copy the code
public class PostVO {
    private Long id;
    private boolean like;
    private int likeNum;
}
Copy the code

In one method, I tried to map an object from PostDO to PostVO, so I did the following:

public class Application {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();
        PostDO postDO = PostDO.builder().id(3L).likeNum(0).build(); PostVO postVO = modelMapper.map(postDO, PostVO.class); System.out.println(postVO); }}Copy the code

The result looks like this:

PostVO(id=3, like=false, likeNum=0)
Copy the code

There is no exception. The value of the likeNum field increases as the project progresses. When likeNum increases to 2, an exception occurs:

Exception in thread "main" org.modelmapper.MappingException: ModelMapper mapping errors:

1) Converter org.modelmapper.internal.converter.BooleanConverter@497470ed failed to convert int to boolean.

1 error
	at org.modelmapper.internal.Errors.throwMappingExceptionIfErrorsExist(Errors.java:380)
	at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:79)
	at org.modelmapper.ModelMapper.mapInternal(ModelMapper.java:554)
	at org.modelmapper.ModelMapper.map(ModelMapper.java:387)
	at Application.main(Application.java:7)
Caused by: org.modelmapper.MappingException: ModelMapper mapping errors:

1) Error mapping 2 to boolean

1 error
	at org.modelmapper.internal.Errors.toMappingException(Errors.java:258)
	at org.modelmapper.internal.converter.BooleanConverter.convert(BooleanConverter.java:49)
	at org.modelmapper.internal.converter.BooleanConverter.convert(BooleanConverter.java:27)
	at org.modelmapper.internal.MappingEngineImpl.convert(MappingEngineImpl.java:298)
	at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:108)
	at org.modelmapper.internal.MappingEngineImpl.setDestinationValue(MappingEngineImpl.java:238)
	at org.modelmapper.internal.MappingEngineImpl.propertyMap(MappingEngineImpl.java:184)
	at org.modelmapper.internal.MappingEngineImpl.typeMap(MappingEngineImpl.java:148)
	at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:113)
	at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:70)...3 more
Copy the code

Int cannot be cast to Boolean. ModelMapper maps the like field to likeNum. So how does ModelMapper map? Let’s take a look at the source code of ModelMapper.

ModelMapper uses reflection to get the fields of the target class and generate the expected matching key-value pairs, something like this.

These key-value pairs are then iterated over looking for matching fields in the source class. First, it determines whether there is a mapping based on the target field.

Mapping existingMapping = this.typeMap.mappingFor(destPath);
if (existingMapping == null) {
    this.matchSource(this.sourceTypeInfo, mutator);
    this.propertyNameInfo.clearSource();
    this.sourceTypes.clear();
}
Copy the code

If not, the matchSource method is called to look for matching fields in the source class based on the matching rules. During the matching process, the system checks whether the type of the target field exists in the type list. If so, you can add the matching field to the mappings based on its name. If not, you need to determine whether there is a converter in the converterStore that can be applied to the field.

if (this.destinationTypes.contains(destinationMutator.getType())) {
    this.mappings.add(new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), true));
} else{ TypeMap<? ,? > propertyTypeMap =this.typeMapStore.get(accessor.getType(), destinationMutator.getType(), (String)null);
    PropertyMappingImpl mapping = null;
    if(propertyTypeMap ! =null) { Converter<? ,? > propertyConverter = propertyTypeMap.getConverter();if (propertyConverter == null) {
            this.mergeMappings(propertyTypeMap);
        } else {
            this.mappings.add(new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), propertyTypeMap.getProvider(), propertyConverter));
        }

        doneMatching = this.matchingStrategy.isExact();
    } else {
        Iterator var9 = this.converterStore.getConverters().iterator();

        while(var9.hasNext()) { ConditionalConverter<? ,? > converter = (ConditionalConverter)var9.next(); MatchResult matchResult = converter.match(accessor.getType(), destinationMutator.getType());if(! MatchResult.NONE.equals(matchResult)) { mapping =new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), false);
                if (MatchResult.FULL.equals(matchResult)) {
                    this.mappings.add(mapping);
                    doneMatching = this.matchingStrategy.isExact();
                    break;
                }

                if (!this.configuration.isFullTypeMatchingRequired()) {
                    this.partiallyMatchedMappings.add(mapping);
                    break; }}}}if (mapping == null) {
        this.intermediateMappings.put(accessor, new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), false)); }}Copy the code

The default converters are 11:

After finding the corresponding Converter, the Converter’s map method returns a MatchResult that has three types of results: FULL, PARTIAL, and NONE. Notice that there is a partial match, the pit I stepped on, and likeNum is defined as a partial match when matching like. Therefore, when likeNum is greater than 2, it cannot be cast to Boolean.

There are two ways to solve this problem. One is to specify that the field name must match completely in the setting. The other is to define the matching policy as strict.

The setting method is as follows:

modelMapper.getConfiguration().setFullTypeMatchingRequired(true);
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
Copy the code

At this point, ModelMapper picks out the source fields that are more appropriate, but if the match requirements are not high, ModelMapper may filter out multiple fields that match the criteria, so further filtering is required.

PropertyMappingImpl mapping;
if (this.mappings.size() == 1) {
    mapping = (PropertyMappingImpl)this.mappings.get(0);
} else {
    mapping = this.disambiguateMappings();
    if (mapping == null&&!this.configuration.isAmbiguityIgnored()) {
        this.errors.ambiguousDestination(this.mappings); }}Copy the code

Here we see that if only one result is matched, that result is returned; If there are multiple, the disambiguateMappings method is called to remove the miscellaneous results. So let’s look at this method.

private PropertyMappingImpl disambiguateMappings(a) {
    List<ImplicitMappingBuilder.WeightPropertyMappingImpl> weightMappings = new ArrayList(this.mappings.size());
    Iterator var2 = this.mappings.iterator();

    while(var2.hasNext()) {
        PropertyMappingImpl mapping = (PropertyMappingImpl)var2.next();
        ImplicitMappingBuilder.SourceTokensMatcher matcher = this.createSourceTokensMatcher(mapping);
        ImplicitMappingBuilder.DestTokenIterator destTokenIterator = new ImplicitMappingBuilder.DestTokenIterator(mapping);

        while(destTokenIterator.hasNext()) {
            matcher.match(destTokenIterator.next());
        }

        double matchRatio = (double)matcher.matches() / ((double)matcher.total() + (double)destTokenIterator.total());
        weightMappings.add(new ImplicitMappingBuilder.WeightPropertyMappingImpl(mapping, matchRatio));
    }

    Collections.sort(weightMappings);
    if (((ImplicitMappingBuilder.WeightPropertyMappingImpl)weightMappings.get(0)).ratio == ((ImplicitMappingBuilder.WeightPropertyMappingImpl)weightMappings.get(1)).ratio) {
        return null;
    } else {
        return ((ImplicitMappingBuilder.WeightPropertyMappingImpl)weightMappings.get(0)).mapping; }}Copy the code

ModelMapper defines a weight to judge whether the source field is ambiguous. Here, according to the camel rule (which can also be set to underscore), the source and target field names are split, and a matching ratio is obtained based on the number of matches/number of source tokens + number of target tokens. The higher the ratio is, the higher the matching degree is. Finally, the field with the largest matching weight is obtained. Other fields are considered ambiguous.

Up to now, the default ModelMapper map method of the working principle has been introduced, there may be some missing details, or where there is not clear, welcome to discuss with me. When using ModelMapper, you must pay attention to the field name. If there is a similar field name, you must carefully check whether the match is correct, and if necessary, use a strict match strategy.