【问题标题】:ModelMapper - Illegal SourceGetter error when defining a explicit mappingModelMapper - 定义显式映射时出现非法 SourceGetter 错误
【发布时间】:2020-12-30 05:42:28
【问题描述】:

第 1 部分

我正在使用 Java ModelMapper 库 (http://modelmapper.org/) 来管理我的实体和 DTO 之间的映射。我有一个联系人(实体)和一个联系人视图(DTO)。 我在 ContactView 中有一个在 Contact 中不存在的字符串字段,称为“type”。 它的值应该只是实体子类的名称。 我试图像这样制作这个自定义映射:

modelMapper.typeMap(Contact.class, ContactView.class).addMappings(mapper -> {   
   mapper.map(src -> src.getClass().getSimpleName(), ContactView::setType);
});

我在以下位置收到编译错误: mapper.map(src -> src.getClass().getSimpleName(), ContactView::setType);

定义了非法的 SourceGetter

1 处错误 org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.3.2.jar:5.3.2] 在 org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:653) ~[spring-beans-5.3.2.jar:5.3.2] ...省略了33个常用框架

我什至尝试使用转换器,结果相同:

modelMapper.typeMap(Contact.class, ContactView.class).addMappings(mapper -> {
  Converter<Class, String> toName = ctx -> ctx.getSource() == null ? null : ctx.getSource().getSimpleName();
  mapper.using(toName).map(Contact::getClass, ContactView::setType);
});

你知道如何解决这个问题吗?

第 2 部分

按照建议的答案,我尝试向 ModelMapper 添加一个转换器类。这是我配置 ModelMapper Bean 的地方:

@Configuration
public class Mapper {
    @Autowired
    private ContactTypeRepository contactTypeRepository;

    @Bean
    public ModelMapper getMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STRICT);

        modelMapper.typeMap(ContactTag.class, ReferenceEntityView.class).addMappings(mapper -> {
            mapper.map(src -> src.getTag().getCode(), ReferenceEntityView::setCode);
            mapper.map(src -> src.getTag().getValue(), ReferenceEntityView::setValue);
        });

        modelMapper.typeMap(Person.class, PersonView.class).addMappings(mapper -> {
            mapper.skip(PersonView::setName);
            mapper.map(Person::getName, PersonView::setLastName);
        });

        modelMapper.addConverter(new ContactConverter());

        return modelMapper;
    }

    class ContactConverter implements Converter<Contact, ContactView>  {
        private ModelMapper localMapper = new ModelMapper();

        @Override
        public ContactView convert(MappingContext<Contact, ContactView> context) {
            Contact contact = context.getSource();
            ContactView contactView = localMapper.map(contact, ContactView.class);
            ContactType contactType = contactTypeRepository.getByCode(context.getSource().getClass().getSimpleName().toLowerCase());
            contactView.setType(localMapper.map(contactType, ReferenceEntityView.class));
            return contactView;
        }
    }
}

这是我使用 ModelMapper Bean 生成 DTO 的地方:

@RestController
@RequestMapping(value = "/contacts")
public class ContactController {
    @Autowired
    private ContactRepository contactRepository;
    @Autowired
    private ModelMapper modelMapper;
    
    @GetMapping(value = "/{id}")
    @ResponseStatus(HttpStatus.OK)
    public ContactView findById(@PathVariable("id") Long id){
        Contact c = contactRepository.getOne(id);
        ContactView cv = modelMapper.map(c, ContactView.class);
        return cv;
    }
}

由于某种原因,未调用 Converter 的 convert 方法,并且 ContactView 对象的“type”字段为空。 ModelMapper Bean 上的其他映射工作正常。

【问题讨论】:

    标签: java spring modelmapper


    【解决方案1】:

    这是因为 ModelMapper 的实现而发生的

     public boolean isValid(M member) {
      return !Modifier.isStatic(member.getModifiers()) && !member.isSynthetic();
    }
    

    isSynthetic 方法的文档说

    如果此成员是由编译器引入的,则返回 true;返回 否则为假。返回:当且仅当此成员是 由编译器引入。

    我猜这就是它失败异常的原因。

    对于类似的情况,我们引入了一个特定的映射器类,使用 modelMapper 作为基础映射器并设置其他字段:

     class ContactMapper{
     ...
     public ContactView toView(Contact contact){
     ContactView contactView = modelMapper.map(contact,ContactView.class);
     contactView.setType(contact.getClass().getSimpleName());
     return contactView;
     }
    

    为了使其与整体映射保持一致,您可以将其定义为转换器并将其注册到您的映射中

    class ContactConverter implements Converter<Contact, ContactView>  {
            private final ModelMapper localMapper = new ModelMapper();
    
            @Override
            public ContactView convert(MappingContext<Contact, ContactView> context) {
                Contact contact = context.getSource();
                ContactView contactView = localMapper.map(contact, ContactView.class);
                contactView.setType(contact.getClass().getSimpleName());
                return contactView;
            }
        }
    
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.addConverter(new ContactConverter());
    

    【讨论】:

    • 谢谢酷。所以基本上我们在 ModelMapper 之外进行自定义映射,在我们从中获取 DTO 对象之后。尽管我们没有将映射逻辑仅保留在 ModelMapper 下,但在这种情况下并不令人担忧。但是如果我对 ContactView 的孩子有类似的问题(例如,一个联系人可以有多个地址)怎么办。您是否建议在 ModelMapper 返回 DTO 后对其进行迭代?好像效率不高。
    • 你是对的博士。法特卡我们也为此使用转换器。我附上了一个例子。
    • 我尝试了附加的示例,但由于某种原因,转换器没有执行。我在convert方法里面加了一个断点,执行时没有到达:ContactView cv = modelMapper.map(c, ContactView.class)。 cv.type 也是 null
    • 您确定将转换器添加到模型映射器中吗?
    • 我相信是的。我在问题的第 2 部分附上了代码。感谢您的帮助。
    【解决方案2】:

    试试我的图书馆beanknife。它是一个注释处理器。这意味着 jdk 会在你编译项目之前为你生成类。所以它在编译时完成工作。您可以像使用自己编写的任何其他类一样使用生成的类。而且你可以看到生成的类的来源,所以没有更多的魔法。 该库能够为您生成 DTO 类。你不需要改变原来的类。除了在原类上配置注解,还可以选择新建一个配置类,在其之上配置注解。该库支持复制和继承原始类的所有属性,并在此基础上删除修改或添加属性。对于您的问题:

    // this will generate a DTO class named "ContactView". 
    // (This is the default name which just append 'View') 
    // You can change this name using genName attribute.
    @ViewOf(value=Contact.class, includePattern = ".*")
    public class ContactViewConfigure {
        // Add a new property. 
        // Make sure there is no property named 'type' already in Contact. 
        // Or you need use @OverrideViewProperty
        // There are multi way to add new property.
        // In this way, you use a public static method accept the original class instance as the unique argument. 
        // The new property name is decide by the attribute 'type' of @NewViewProperty. 
        // So the method name is not important.
        @NewViewProperty("type")
        public static String type(Contact contact) {
            return contact.getClass().getSimpleName()
        }
    }
    
    // This is the generated class.
    public class ContactView {
        // other properties
        ...
        private String type;
        // getters and setters (By default only getters are generated)
        ...
        // many constructors
        ...
        public static ContactView read(Contact source) {
            ContactView out = new ContactView();
            // initialize other properties
            ...
            out.type = ContactViewConfigure.type(source);
            // initialize other properties
            ...
            return out;
        }
    
        // other read method, such as read list, set and map.
        ...
        // other generated methods
        ...
    }
    
    // use like this.
    Contact contact = ...
    ContactView dto = ContactView.read(contact);
    

    在某些情况下,beanknife 比 ModelMapper 强大得多。例如,如果发生错误,您可以检查生成的类的来源(通常位于 /target/generated-source/annotations,在 IDE 上可能会有所不同),并查看原因。如果真的是bug,可以提交到github,我会尽快处理。

    这里有更多examples

    【讨论】:

    • 感谢分享。这似乎很有希望,不过,我能够使用 ModelMapper 继续前进。我更新了库的版本,并对我的映射和转换器的编码方式进行了很多重构。如果以后我发现更多问题并决定更换MM,我会看看你的解决方案。
    • ModelMapper 和其他一些映射库专注于映射数据结构,而 beanknife 专注于生成 DTO。虽然它们之间有一些重叠的功能,但它们有不同的侧重点。对于已有的 DTO,使用映射库更方便,但如果要添加新的 DTO,beanknife 更合适。但是 beanknife 和 ModelMapper 可以一起工作而不会互相冲突
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多