如何在 Dozer 中映射不可变对象的集合

How to map collections of immutable objects in Dozer

灵感来自 , I have wrote a custom conveter (you can find the whole working example in the Github repo)。推土机在以下之间转换:

public class MyEntity {
    private List<ObjectId> attachmentIds;

    public List<ObjectId> getAttachmentIds() { return attachmentIds; }

    public void setAttachmentIds(List<ObjectId> attachmentIds) {
        this.attachmentIds = attachmentIds;
    }
}

及其 DTO:

public class MyEntityDto {
    private List<FileDataDto> attachments;

    public List<FileDataDto> getAttachments() { return attachments; }

    public void setAttachments(List<FileDataDto> attachments) {
        this.attachments = attachments;
    }
}

MyEntity 仅保存存储在 Mongo 数据库中的文件的 ID。它在 JSON 中发送到前端的 DTO 应该包含文件的 ID 和文件名(这是 FileDataDto class 的内容)。我的转换器:

public class FileIdToFileDataConverter extends DozerConverter<ObjectId, FileDataDto> {
    public FileIdToFileDataConverter() {super(ObjectId.class, FileDataDto.class); }

    @Override
    public FileDataDto convertTo(ObjectId source, FileDataDto destination) {
        if (source == null) {
            return null;
        }
        FileDataDto fileData = destination == null ? new FileDataDto() : destination;
        fileData.setId(source.toString());
        // fetch the file from repository and update the name from db
        fileData.setFilename("myfile.txt");
        return fileData;
    }

    @Override
    public ObjectId convertFrom(FileDataDto source, ObjectId destination) {
        return source == null ? null : new ObjectId(source.getId());
    }
}

转换按预期在 MyEntity -> MyEntityDto 方向进行。然而,它在相反的情况下失败了。它使用由 Dozer 创建的 ObjectId(作为 destination 参数传递)而不是转换器返回的那个。本次测试

@Test
public void dtoToMyEntity() {
    MyEntityDto dto = new MyEntityDto();
    FileDataDto fileData = new FileDataDto();
    fileData.setFilename("file.txt");
    fileData.setId(new ObjectId().toString());
    dto.setAttachments(Arrays.asList(fileData));
    MyEntity myEntity = mapper.map(dto, MyEntity.class);
    assertEquals(fileData.getId(), myEntity.getAttachmentIds().get(0).toString());
}

示例消息失败:

org.junit.ComparisonFailure: 
  Expected :56b0a9d110a937fc32a6db18
  Actual   :56b0a9d110a937fc32a6db19

你可以在 Github repo.

中找到我使用的整个测试和配置

如何让转换器双向工作?

它与推土机中的错误有关,导致通过 API 映射时不使用自定义转换器:https://github.com/DozerMapper/dozer/issues/242

因此您可以通过 xml:

提供任一映射
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://dozer.sourceforge.net
          http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <configuration>
        <custom-converters>
            <converter type="com.example.mapping.FileIdToFileDataConverter">
                <class-a>org.bson.types.ObjectId</class-a>
                <class-b>com.example.mapping.entity.FileDataDto</class-b>
            </converter>
        </custom-converters>
    </configuration>
    <mapping>
        <class-a>com.example.mapping.entity.MyEntity</class-a>
        <class-b>com.example.mapping.entity.MyEntityDto</class-b>
        <field>
            <a>attachmentIds</a>
            <b>attachments</b>
            <a-hint>org.bson.types.ObjectId</a-hint>
            <b-hint>com.example.mapping.entity.FileDataDto</b-hint>
        </field>
    </mapping>
</mappings>

然后

mapper.setMappingFiles(Arrays.asList("dozerconfig.xml"));

或者,如果您不想使用 xml,您可以创建一个使用自己的 ObjectIdFactory:

的解决方法
mapping(type(ObjectId.class).beanFactory(ObjectIdFactory.class), FileDataDto.class)
    .fields(this_(), this_(), customConverter(FileIdToFileDataConverter.class));

和工厂class

public class ObjectIdFactory implements BeanFactory {
    @Override
    public Object createBean(Object source, Class<?> sourceClass, String targetBeanId) {
        if (source == null) {
            return null;
        }
        if (source instanceof ObjectId) {
            return source; // we can return source, because it's immutable
        }
        if (source instanceof String) {
            return new ObjectId((String) source);
        }
        if (source instanceof FileDataDto) {
            return new ObjectId(((FileDataDto) source).getId());
        }
        throw new MappingException("ObjectId should be of type ObjectId, String or FileDataDto");
    }
}

此解决方法有效的原因以及 ID 不匹配的原因

Dozer 默认使用 class 的无参数构造函数来实例化空值。 ObjectId 是不可变的 class 并且它的无参数构造函数根据时间戳创建新实例。

一个更简单的替代方法是使用 MapStruct It supports immutables out of the box (including Lombok's and Immutable's builders).

最小代码示例(来自文档):

@Mapper
public interface CarMapper { 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDto carToCarDto(Car car);
}

// Usage:
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);