ModelMapper: how to keep converters registered at mapper level when using a TypeMap with custom mapping

huangapple go评论74阅读模式
英文:

ModelMapper: how to keep converters registered at mapper level when using a TypeMap with custom mapping

问题

我使用ModelMapper将模型转换为DTO。我有一堆默认转换器用于处理空值,在映射器级别进行了注册,如下所示:

modelMapper.addConverter(new Converter<String, String>() {
    @Override
    public String convert(MappingContext<String, String> context) {
        if (context.getSource() == null) {
            return "global null converter was here";
        }
        return context.getSource();
    }
});

这在属性名称在转换的两侧相同时可以很好地处理简单映射。转换器按预期用于处理空值。

现在,如果我需要进行更复杂的转换,使用类型映射上的 .map(getter, setter) 来处理不同的属性名称,全局转换器就不再被调用了。
我不希望在配置类型映射时丢弃全局转换器。

我该如何修复这个问题?

这是一个使用了ModelMapper 2.3.8(当天最新版本)的示例代码(为了简洁起见使用了Lombok):

@Data @AllArgsConstructor @NoArgsConstructor class A { String a; String b;}
    
@Data @AllArgsConstructor @NoArgsConstructor class B { String a; String b; }
    
@Data @AllArgsConstructor @NoArgsConstructor class C { String x; String y;}
    
public class MapperTestCase {
    
    public static void main(String[] args) throws IOException {

        A a = new A("aaa", "bbb");
        
        ModelMapper modelMapper = new ModelMapper();
        final TypeMap<A, B> AtoBTypeMap = modelMapper.createTypeMap(A.class, B.class);
        B b = AtoBTypeMap.map(a);
        System.out.println("conversion with no converter A -> B: " + a + " -> " + b);

        a = new A(null, null);
        b = AtoBTypeMap.map(a);
        System.out.println("conversion with no converter A -> B: " + a + " -> " + b);

        // 添加一个全局/回退转换器,用于转换所有空字符串值。
        modelMapper.addConverter(new Converter<String, String>() {
            @Override
            public String convert(MappingContext<String, String> context) {
                if (context.getSource() == null) {
                    return "global null converter was here";
                }
                return context.getSource();
            }
        });            

        final TypeMap<B, A> BtoATypeMap = modelMapper.typeMap(B.class, A.class);
        a = BtoATypeMap.map(b);
        System.out.println("conversion with global converter B -> A: " + b + " -> " + a);
        
        // 为 B 到 C 的类型映射添加一个局部转换器
        BtoATypeMap.addMappings(mapper -> mapper.using(ctx -> {
            if (ctx.getSource() == null) {
                return "local converter was here";
            } else return ctx.getSource();
        }).map(B::getA, (w, x) -> w.setA(String.valueOf(x))));

        // 在这个转换中,应该同时使用全局和局部转换器
        a = BtoATypeMap.map(b);
        System.out.println("conversion with global and local converter B -> A: " + b + " -> " + a);

        // 创建一个新的类型映射,将 B 转换为 C,将 B::a 映射到 C::x,将 B::b 映射到 C::y
        final TypeMap<B, C> BtoCTypeMap = modelMapper.typeMap(B.class, C.class);
        
        // 为这个类型映射添加一个局部转换器
        BtoCTypeMap.addMappings(mapper -> mapper.using(ctx -> {
            if (ctx.getSource() == null) {
                return "local converter was here";
            } else return ctx.getSource();
        }).map(B::getA, (w, x) -> w.setX(String.valueOf(x))));
        
        BtoCTypeMap.addMapping(B::getB, C::setY);
        // 先进行一个没有空值的 B 实例的转换,按预期工作
        b = new B("some", "data");
        C c = BtoCTypeMap.map(b);
        System.out.println("conversion with global and local converter B -> C: " + b + " -> " + c);

        // 现在进行一个带有空值的 B 实例的转换,会使用局部转换器,但不会使用在映射器级别定义的全局转换器。为什么呢?
        b = new B();
        c = BtoCTypeMap.map(b);
        System.out.println("conversion with global and local converter B -> C: " + b + " -> " + c);
    }
}

输出结果为:

conversion with no converter A -> B: A(a=aaa, b=bbb) -> B(a=aaa, b=bbb)
conversion with no converter A -> B: A(a=null, b=null) -> B(a=null, b=null)
conversion with global converter B -> A: B(a=null, b=null) -> A(a=global null converter was here, b=global null converter was here)
conversion with global and local converter B -> A: B(a=null, b=null) -> A(a=local converter was here, b=global null converter was here)
conversion with global and local converter B -> C: B(a=some, b=data) -> C(x=some, y=data)
conversion with global and local converter B -> C: B(a=null, b=null) -> C(x=local converter was here, y=null)

最后一行的预期输出应为 C(x=local converter was here, y=global null converter was here)

英文:

I use ModelMapper to convert Models to DTOs. I have a bunch of default converters for null values that have been registered at the mapper level like this:

        modelMapper.addConverter(new Converter&lt;String, String&gt;() {
@Override
public String convert(MappingContext&lt;String, String&gt; context) {
if (context.getSource() == null) {
return &quot;global null converter was here&quot;;
}
return context.getSource();
}
});

This works fine with simple mapping when the properties name are the same on both side of the conversion.
The converter is used to handle null values as expected.

Now if I need to do a more complex conversions with different properties name by using .map(getter, setter) on the type map, the global converters are not called anymore.
I don't want the global converters to be discarded when configuring the typemap.

How can I fix that ?

Here is a sample code (with lombok for code brevity) that use ModelMapper 2.3.8, the today's latest version:

@Data @AllArgsConstructor @NoArgsConstructor class A { String a; String b;}
@Data @AllArgsConstructor @NoArgsConstructor class B { String a; String b; }
@Data @AllArgsConstructor @NoArgsConstructor class C { String x; String y;}
public class MapperTestCase {
public static void main(String[] args) throws IOException {
A a = new A(&quot;aaa&quot;, &quot;bbb&quot;);
ModelMapper modelMapper = new ModelMapper();
final TypeMap&lt;A, B&gt; AtoBTypeMap = modelMapper.createTypeMap(A.class, B.class);
B b = AtoBTypeMap.map(a);
System.out.println(&quot;conversion with no converter A -&gt; B: &quot; + a + &quot; -&gt; &quot; + b);
a = new A(null, null);
b = AtoBTypeMap.map(a);
System.out.println(&quot;conversion with no converter A -&gt; B: &quot; + a + &quot; -&gt; &quot; + b);
// Add a global/fallback converter that should convert all null String values.
modelMapper.addConverter(new Converter&lt;String, String&gt;() {
@Override
public String convert(MappingContext&lt;String, String&gt; context) {
if (context.getSource() == null) {
return &quot;global null converter was here&quot;;
}
return context.getSource();
}
});            
final TypeMap&lt;B, A&gt; BtoATypeMap = modelMapper.typeMap(B.class, A.class);
a = BtoATypeMap.map(b);
System.out.println(&quot;conversion with global converter B -&gt; A: &quot; + b + &quot; -&gt; &quot; + a);
// add a local converter for the B to C type mape only
BtoATypeMap.addMappings(mapper -&gt; mapper.using(ctx -&gt; {
if (ctx.getSource() == null) {
return &quot;local converter was here&quot;;
} else return ctx.getSource();
}).map(B::getA, (w, x) -&gt; w.setA(String.valueOf(x))));
// in this conversion both converter (global and local) should be used
a = BtoATypeMap.map(b);
System.out.println(&quot;conversion with global and local converter B -&gt; A: &quot; + b + &quot; -&gt; &quot; + a);
// a new typeMap that will transform a B into a C, mapping B::a to C::x and B::b to C::y
final TypeMap&lt;B, C&gt; BtoCTypeMap = modelMapper.typeMap(B.class, C.class);
// a local converter for this type map
BtoCTypeMap.addMappings(mapper -&gt; mapper.using(ctx -&gt; {
if (ctx.getSource() == null) {
return &quot;local converter was here&quot;;
} else return ctx.getSource();
}).map(B::getA, (w, x) -&gt; w.setX(String.valueOf(x))));
BtoCTypeMap.addMapping(B::getB, C::setY);
// first a conversion with a B instance without null values, works as expected
b = new B(&quot;some&quot;, &quot;data&quot;);
C c = BtoCTypeMap.map(b);
System.out.println(&quot;conversion with global and local converter B -&gt; C: &quot; + b + &quot; -&gt; &quot; + c);
// now a conversion with a B instance wirth null values, the local converer will be used, but not the global one defined at the mapper level. Why ?
b = new B();
c = BtoCTypeMap.map(b);
System.out.println(&quot;conversion with global and local converter B -&gt; C: &quot; + b + &quot; -&gt; &quot; + c);
}
}

The output is:

conversion with no converter A -&gt; B: A(a=aaa, b=bbb) -&gt; B(a=aaa, b=bbb)
conversion with no converter A -&gt; B: A(a=null, b=null) -&gt; B(a=null, b=null)
conversion with global converter B -&gt; A: B(a=null, b=null) -&gt; A(a=global null converter was here, b=global null converter was here)
conversion with global and local converter B -&gt; A: B(a=null, b=null) -&gt; A(a=local converter was here, b=global null converter was here)
conversion with global and local converter B -&gt; C: B(a=some, b=data) -&gt; C(x=some, y=data)
conversion with global and local converter B -&gt; C: B(a=null, b=null) -&gt; C(x=local converter was here, y=null)

The expected output for the last line is C(x=local converter was here, y=global null converter was here)

答案1

得分: 2

我必须承认,我通常使用MapStruct或Dozer,但偶尔也会使用ModelMapper。

话虽如此,我将尝试解释我在使用这个库时遵循的思维模式:希望它能帮助您理解您的问题。

当您在ModelMapper中为源类和目标类之间定义映射时,实际上是在定义它们属性之间的对应关系,即mapping

如果您未在源类的某个属性和目标类的另一个属性之间定义显式映射,将会发生隐式映射。

这种隐式映射基于多种匹配策略,但对于我们的问题,我们可以确定它基于属性名称匹配。

如果在ModelMapper级别上定义了一个Converter,它将仅应用于属性mapping,前提是在源属性和目标属性之间没有提供显式属性mapping。原因如下:如果在TypeMap中的任何属性之间定义了显式属性mapping,使用addMappingaddMappings方法,为该显式mapping提供的配置(源getter和目标setter、转换器、预转换器、后转换器)将是在mapping过程中唯一生效的配置,无论您在更高级别的映射中定义了什么。

您可以通过调试程序并逐行查看库中定义的基础属性mapping来轻松测试这个事实。

出于这个原因,我认为不可能实现这样的全局行为:您可以通过可能实现一个工厂方法来重复执行它,正如其他答案中建议的那样,或者更好的是,创建特定的Converter类,您可以将其实例化并设置为每个TypeMap和属性mapping中的转换器(或者在您的用例中,可能是后转换器)。

在这里,有一个优秀的帖子,可以为您提供关于在使用ModelMapper时底层发生了什么的更好的解释。

英文:

I must recognize that I normally use MapStruct or Dozer but, from time to time, I have used ModelMapper.

That been said, I will try to explain the mental model I follow when working with this library: I hope it helps you in understand your problem.

When you define a map between a source and a destination class in ModelMapper, you are actually defining the correspondence, the mappings, between their properties.

There is an implicit mapping that takes place if you do not define an explicit one between one property in the source class and another in the destination class.

This implicit mapping is based on several matching policies but we can safely say for our problem that it is based on property name matching.

If you define a Converter at the ModelMapper level, it will be applied to a property mapping only if one explicit property mapping is not provided, for the following reason: if you define an explicit property mapping between any properties in a TypeMap, by using the methods addMapping or addMappings, the configuration provided for that explicit mapping (source getter and destination setter, converters, pre-converters, post-converters) will be the only that will take place in the mapping process, no matter what you define at a higher mapping level.

You can easily test this fact by debugging your program and see, line by line, how the underlying property mappings are defined by the library.

For that reason I think it is not possible to implement such a global behavior: what you can do is repeat it by possibly implementing a factory method as suggested in other answer or better, by creating specific Converter classes that you can instantiate and set as converters (or, maybe, post-converters, in your use case) of every TypeMap and property mapping in which it is required.

There is an excelent post here in stackoverflow that will provide you a great and better explanation about what is happening under the hood when you use ModelMapper.

答案2

得分: 2

如果您想创建一个通用的属性转换器,可以尝试类似这样的方法:

Converter<String, String> stringPropertyConverter = new Converter<String, String>() {
    @Override
    public String convert(MappingContext<String, String> context) {
        if (context.getSource() == null) {
            return "全局空值转换器在此";
        }
        return context.getSource();
    }
};

ModelMapper modelMapper = new ModelMapper() {
    @Override
    public <S, D> TypeMap<S, D> typeMap(Class<S> sourceType, Class<D> destinationType) {
        TypeMap<S, D> typeMap = super.typeMap(sourceType, destinationType);
        typeMap.setPropertyConverter(stringPropertyConverter);
        return typeMap;
    }
}; 
在映射过程中通常会遇到使用转换器的问题首先ModelMapper为您的类定义转换器然后在下一步中它会为类的字段查找合适的转换器在第一种情况下您的转换器按顺序放置如下

    "TypeMap[String -> String]"
    "TypeMap[B -> A]"
    "TypeMap[A -> B]"

在第二种情况下

    "TypeMap[B -> C]"
    "TypeMap[String -> String]"
    "TypeMap[B -> A]"
    "TypeMap[A -> B]"

而将B转换为C的转换器是类中任何字段的适用转换器

<details>
<summary>英文:</summary>

If You wanna create general propertyConverter you can try someshing like this

            Converter&lt;String, String&gt; stringPropertyConverter = new Converter&lt;String, String&gt;() {
            @Override
            public String convert(MappingContext&lt;String, String&gt; context) {
                if (context.getSource() == null) {
                    return &quot;global null converter was here&quot;;
                }
                return context.getSource();
            }
        };

        ModelMapper modelMapper = new ModelMapper() {
            @Override
            public &lt;S, D&gt; TypeMap&lt;S, D&gt; typeMap(Class&lt;S&gt; sourceType, Class&lt;D&gt; destinationType) {
                TypeMap&lt;S, D&gt; typeMap = super.typeMap(sourceType, destinationType);
                typeMap.setPropertyConverter(stringPropertyConverter);
                return typeMap;
            }

        }; 
Generally problem in order convertors which using in mapping process.
At first modelMapper define convertor for your class, on the next step it search suitable convertor for field of class.
In the firs case your converters placed in order 

    &quot;TypeMap[String -&gt; String]&quot;
    &quot;TypeMap[B -&gt; A]&quot;
    &quot;TypeMap[A -&gt; B]&quot;

in the second case 

    &quot;TypeMap[B -&gt; C]&quot;
    &quot;TypeMap[String -&gt; String]&quot;
    &quot;TypeMap[B -&gt; A]&quot;
    &quot;TypeMap[A -&gt; B]&quot;

and convertor B to C is suitable convertor for any of your fields in your class.

</details>



# 答案3
**得分**: 1

我认为我找到了解决方案

这是因为你的类C的属性名称与你的类A和B不同如果你将x重命名为a将y重命名为b输出就会是正确的

你现在的问题是,“为什么会这样工作”,简单地说因为modelMapper只有在两个对象之间的名称相同的情况下才应用转换器我不认为modelMapper提供了一个真正的全局转换器解决方案以按照你的意愿忽略属性名称

关于你的代码我认为你应该使用Java 8的功能

```Java
modelMapper.addConverter(new Converter<String, String>() {
    @Override
    public String convert(MappingContext<String, String> context) {
        if (context.getSource() == null) {
             return "全局空转换器在此";
        }
        return context.getSource();
    }
});

可以重写为:

modelMapper.addConverter(context -> context.getSource() == null ? "全局空转换器在此" : context.getSource());

// 或者更好,因为你提取了映射逻辑:

modelMapper.addConverter(mySuperConverter());

private static Converter<String, String> mySuperConverter() {
    return context -> context.getSource() == null ? "全局空转换器在此" : context.getSource();
}

英文:

I think i found the solution :

it's because your class C have different attribute names than your class A and B. If you rename x to a and y to b, the output will be good.

Your question now is "why is this working like that", simply because modelMapper apply the converter only if the name between the 2 objects are the same. I don't think modelMapper provide a solution for a real "global" converter by ignoring attributes name as you wish.

About your code, i think you should use Java 8 functionality :

modelMapper.addConverter(new Converter&lt;String, String&gt;() {
    @Override
    public String convert(MappingContext&lt;String, String&gt; context) {
        if (context.getSource() == null) {
             return &quot;global null converter was here&quot;;
        }
        return context.getSource();
    }
});

can be rewrite :

modelMapper.addConverter(context -&gt; context.getSource() == null ? &quot;global null converter was here&quot; : context.getSource());

// or much better because you extract your mapping logic :

modelMapper.addConverter(mySuperConverter());

private static Converter&lt;String, String&gt; mySuperConverter() {
    return context -&gt; context.getSource() == null ? &quot;global null converter was here&quot; : context.getSource();
}

huangapple
  • 本文由 发表于 2020年8月26日 16:22:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/63593503.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定