Руководство по составлению карт с помощью Dozer

1. Обзор

Dozer - это преобразователь Java Bean в Java Bean , который рекурсивно копирует данные из одного объекта в другой, атрибут за атрибутом.

Библиотека не только поддерживает сопоставление имен атрибутов Java Beans, но также автоматически конвертирует между типами - если они разные.

Большинство сценариев конвертации поддерживаются "из коробки", но Dozer также позволяет определять пользовательские конвертации через XML .

2. Простой пример

Для нашего первого примера давайте предположим, что все исходные и целевые объекты данных имеют одинаковые общие имена атрибутов.

Это самое основное отображение, которое можно сделать с помощью Dozer:

public class Source {
    private String name;
    private int age;

    public Source() {}

    public Source(String name, int age) {
        this.name = name;
        this.age = age;
    }

   //standard getters and setters
}

Затем наш целевой файл, Dest.java :

public class Dest {
    private String name;
    private int age;

    public Dest() {}

    public Dest(String name, int age) {
        this.name = name;
        this.age = age;
    }

   //standard getters and setters
}

Мы должны убедиться, что включают конструкторы по умолчанию или нулевые аргументы , так как Dozer использует отражение под капотом.

А в целях производительности давайте сделаем наш маппер глобальным и создадим один объект, который мы будем использовать в наших тестах:

DozerBeanMapper mapper;

@Before
public void before() throws Exception {
    mapper = new DozerBeanMapper();
}

Теперь давайте запустим наш первый тест, чтобы подтвердить, что когда мы создаем объект Source , мы можем отобразить его непосредственно на объект Dest :

@Test
public void givenSourceObjectAndDestClass__whenMapsSameNameFieldsCorrectly__
  thenCorrect() {
    Source source = new Source("Baeldung", 10);
    Dest dest = mapper.map(source, Dest.class);

    assertEquals(dest.getName(), "Baeldung");
    assertEquals(dest.getAge(), 10);
}

Как мы видим, после сопоставления Dozer результатом будет новый экземпляр объекта Dest , который содержит значения для всех полей, имена которых совпадают с именем объекта Source .

В качестве альтернативы, вместо передачи mapper в класс Dest , мы могли бы просто создать объект Dest и передать mapper его ссылку:

@Test
public void givenSourceObjectAndDestObject__whenMapsSameNameFieldsCorrectly__
  thenCorrect() {
    Source source = new Source("Baeldung", 10);
    Dest dest = new Dest();
    mapper.map(source, dest);

    assertEquals(dest.getName(), "Baeldung");
    assertEquals(dest.getAge(), 10);
}

3. Maven Setup

Теперь, когда у нас есть общее представление о том, как работает Dozer, давайте добавим следующую зависимость в pom.xml :

<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>

Последняя версия доступна по ссылке here .

4. Пример преобразования данных

Как мы уже знаем, Dozer может сопоставлять существующий объект другому, если он находит атрибуты с одинаковыми именами в обоих классах.

Однако это не всегда так; и поэтому, если какой-либо из сопоставленных атрибутов относится к разным типам данных, механизм сопоставления Dozer автоматически выполнит преобразование типа данных .

Давайте посмотрим на эту новую концепцию в действии:

public class Source2 {
    private String id;
    private double points;

    public Source2() {}

    public Source2(String id, double points) {
        this.id = id;
        this.points = points;
    }

   //standard getters and setters
}

И класс назначения:

public class Dest2 {
    private int id;
    private int points;

    public Dest2() {}

    public Dest2(int id, int points) {
        super();
        this.id = id;
        this.points = points;
    }

   //standard getters and setters
}

Обратите внимание, что имена атрибутов одинаковы, но их типы данных различны .

В исходном классе id является String , а points является double , тогда как в целевом классе id и points оба являются __integer __s.

Давайте теперь посмотрим, как Dozer правильно обрабатывает преобразование:

@Test
public void givenSourceAndDestWithDifferentFieldTypes__
  whenMapsAndAutoConverts__thenCorrect() {
    Source2 source = new Source2("320", 15.2);
    Dest2 dest = mapper.map(source, Dest2.class);

    assertEquals(dest.getId(), 320);
    assertEquals(dest.getPoints(), 15);
}

Мы передали «320» и 15.2 , String и double в исходный объект, и в результате было 320 и 15, оба __integer __s в целевом объекте.

5. Основные пользовательские сопоставления через XML

Во всех предыдущих примерах, которые мы видели, объекты данных источника и назначения имеют одинаковые имена полей, что позволяет легко сопоставлять их с нашей стороны.

Однако в реальных приложениях будет множество случаев, когда два отображаемых нами объекта данных не будут иметь полей с общим именем свойства.

Чтобы решить эту проблему, Dozer дает нам возможность создать пользовательскую конфигурацию отображения в XML .

В этом XML-файле мы можем определить записи отображения классов, которые механизм отображения Dozer будет использовать, чтобы решить, какой атрибут источника сопоставить с каким атрибутом назначения.

Давайте рассмотрим пример и попробуем демаршировать объекты данных из приложения, созданного французским программистом, к английскому стилю именования наших объектов.

У нас есть объект Person с полями name , nickname и age :

public class Person {
    private String name;
    private String nickname;
    private int age;

    public Person() {}

    public Person(String name, String nickname, int age) {
        super();
        this.name = name;
        this.nickname = nickname;
        this.age = age;
    }

   //standard getters and setters
}

Объект, который мы демонтируем, называется Personne и имеет поля nom , surnom и age :

public class Personne {
    private String nom;
    private String surnom;
    private int age;

    public Personne() {}

    public Personne(String nom, String surnom, int age) {
        super();
        this.nom = nom;
        this.surnom = surnom;
        this.age = age;
    }

   //standard getters and setters
}

Эти объекты действительно достигают той же цели, но у нас есть языковой барьер. Чтобы справиться с этим барьером, мы можем использовать Dozer для отображения французского объекта Personne на наш объект Person

Нам нужно только создать собственный файл сопоставления, чтобы помочь Dozer в этом, мы назовем его dozer mapping.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">
    <mapping>
        <class-a>com.baeldung.dozer.Personne</class-a>
        <class-b>com.baeldung.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

Это самый простой пример пользовательского файла сопоставления XML, который мы можем иметь.

На данный момент достаточно заметить, что у нас есть <mappings> в ​​качестве нашего корневого элемента, который имеет дочерний <mapping> , у нас может быть столько же этих дочерних элементов внутри <mappings> __, сколько существует случаев класса пары, которые нуждаются в настраиваемом отображении.

Обратите также внимание на то, как мы указываем исходный и целевой классы внутри тегов <mapping> </mapping> . Далее следует <field> </field> для каждой пары полей источника и назначения, для которых требуется настраиваемое сопоставление.

Наконец, обратите внимание, что мы не включили поле age в наш файл пользовательского сопоставления. Французское слово для обозначения возраста - все еще возраст, что подводит нас к еще одной важной особенности Dozer.

  • Свойства, имеющие одно и то же имя, не нужно указывать в файле сопоставления XML ** . Dozer автоматически сопоставляет все поля с одинаковым именем свойства из исходного объекта в целевой объект.

Затем мы поместим наш пользовательский файл XML в путь к классам прямо в папке src . Однако, где бы мы ни помещали его в путь к классам, Dozer будет искать по всему пути к классам в поисках указанного файла.

Давайте создадим вспомогательный метод для добавления файлов сопоставления в наш mapper :

public void configureMapper(String... mappingFileUrls) {
    mapper.setMappingFiles(Arrays.asList(mappingFileUrls));
}

Давайте теперь протестируем код:

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper__
  whenMaps__thenCorrect() {
    configureMapper("dozer__mapping.xml");
    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

Как показано в тесте, DozerBeanMapper принимает список пользовательских файлов сопоставления XML и решает, когда использовать каждый из них во время выполнения.

Предполагая, что теперь мы начинаем демаршализацию этих объектов данных между нашим английским приложением и приложением на французском. Нам не нужно создавать другое сопоставление в XML-файле, Dozer достаточно умен, чтобы сопоставлять объекты в обоих направлениях только с одной конфигурацией сопоставления :

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper__
  whenMapsBidirectionally__thenCorrect() {
    configureMapper("dozer__mapping.xml");
    Person englishAppPerson = new Person("Dwayne Johnson", "The Rock", 44);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

И поэтому этот пример теста использует еще одну особенность Dozer - тот факт, что механизм отображения Dozer является двунаправленным , поэтому, если мы хотим отобразить объект назначения на исходный объект, нам не нужно добавлять другое отображение класса в XML-файл.

Мы также можем загрузить пользовательский файл сопоставления извне classpath, если нам нужно, использовать префикс « file: » в имени ресурса.

В среде Windows (например, в тесте ниже) мы, конечно, будем использовать специальный синтаксис файлов Windows.

На коробке Linux мы можем сохранить файл в /home , а затем:

configureMapper("file:/home/dozer__mapping.xml");

И в Mac OS:

configureMapper("file:/Users/me/dozer__mapping.xml");

Если вы запускаете модульные тесты из проекта github (что необходимо), вы можете скопировать файл сопоставления в соответствующее местоположение и изменить входные данные. для метода configureMapper .

Файл сопоставления доступен в папке test/resources проекта GitHub:

@Test
public void givenMappingFileOutsideClasspath__whenMaps__thenCorrect() {
    configureMapper("file:E:\\dozer__mapping.xml");
    Person englishAppPerson = new Person("Marshall Bruce Mathers III","Eminem", 43);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

** 6. Подстановочные знаки и дальнейшая настройка XML

**

Давайте создадим второй пользовательский файл сопоставления с именем dozer mapping2.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">
    <mapping wildcard="false">
        <class-a>com.baeldung.dozer.Personne</class-a>
        <class-b>com.baeldung.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

Обратите внимание, что мы добавили атрибут wildcard к элементу <mapping> </mapping> , которого раньше не было.

По умолчанию wildcard это true . Он сообщает движку Dozer, что мы хотим, чтобы все поля в исходном объекте были сопоставлены с соответствующими полями назначения.

Когда мы устанавливаем false, мы говорим Dozer только отображать поля, которые мы явно указали в XML.

Таким образом, в приведенной выше конфигурации мы хотим отобразить только два поля, оставив age :

@Test
public void givenSrcAndDest__whenMapsOnlySpecifiedFields__thenCorrect() {
    configureMapper("dozer__mapping2.xml");
    Person englishAppPerson = new Person("Shawn Corey Carter","Jay Z", 46);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

Как мы видим из последнего утверждения, поле age назначения осталось 0

7. Пользовательское сопоставление с помощью аннотаций

Для простых случаев отображения и случаев, когда у нас также есть доступ на запись к объектам данных, которые мы хотели бы отобразить, нам, возможно, не потребуется использовать отображение XML.

Сопоставление полей с разными именами с помощью аннотаций очень просто, и мы должны писать гораздо меньше кода, чем в сопоставлении XML, но могут помочь нам только в простых случаях.

Давайте скопируем наши объекты данных в Person2.java и Personne2.java , не меняя поля вообще.

Чтобы реализовать это, нам нужно только добавить аннотацию @ mapper («destinationFieldName») к методам getter в исходном объекте. Вот так:

@Mapping("name")
public String getNom() {
    return nom;
}

@Mapping("nickname")
public String getSurnom() {
    return surnom;
}

На этот раз мы рассматриваем Personne2 как источник, но это не имеет значения из-за двунаправленной природы Dozer Engine.

Теперь, когда весь код, связанный с XML, удален, наш тестовый код стал короче:

@Test
public void givenAnnotatedSrcFields__whenMapsToRightDestField__thenCorrect() {
    Person2 englishAppPerson = new Person2("Jean-Claude Van Damme", "JCVD", 55);
    Personne2 frenchAppPerson = mapper.map(englishAppPerson, Personne2.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

Мы также можем проверить двунаправленность:

@Test
public void givenAnnotatedSrcFields__whenMapsToRightDestFieldBidirectionally__
  thenCorrect() {
    Personne2 frenchAppPerson = new Personne2("Jason Statham", "transporter", 49);
    Person2 englishAppPerson = mapper.map(frenchAppPerson, Person2.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

8. Пользовательское сопоставление API

В наших предыдущих примерах, где мы демаршируем объекты данных из французского приложения, мы использовали XML и аннотации для настройки нашего отображения.

Еще одна альтернатива, доступная в Dozer, похожая на отображение аннотаций, - это отображение API. Они похожи, потому что мы исключаем конфигурацию XML и строго используем код Java.

В этом случае мы используем класс BeanMappingBuilder , определенный в нашем простейшем случае следующим образом:

BeanMappingBuilder builder = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom");
    }
};

Как мы видим, у нас есть абстрактный метод configure () , который мы должны переопределить, чтобы определить наши конфигурации. Затем, как и наши теги <mapping> </mapping> в ​​XML, мы определяем столько __TypeMappingBuilder __s, сколько нам нужно.

Эти компоновщики сообщают Dozer, какой источник для полей назначения мы отображаем. Затем мы передаем BeanMappingBuilder в DozerBeanMapper , как и в случае с файлом сопоставления XML, только с другим API:

@Test
public void givenApiMapper__whenMaps__thenCorrect() {
    mapper.addMapping(builder);

    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

API отображения также является двунаправленным:

@Test
public void givenApiMapper__whenMapsBidirectionally__thenCorrect() {
    mapper.addMapping(builder);

    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

Или мы можем выбрать отображение только указанных полей с помощью этой конфигурации компоновщика:

BeanMappingBuilder builderMinusAge = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom")
              .exclude("age");
    }
};

и наш тест age == 0 вернулся:

@Test
public void givenApiMapper__whenMapsOnlySpecifiedFields__thenCorrect() {
    mapper.addMapping(builderMinusAge);
    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

9. Пользовательские конвертеры

Другой сценарий, с которым мы можем столкнуться при отображении, - это то, где мы хотели бы выполнить пользовательское отображение между двумя объектами .

Мы рассмотрели сценарии, в которых имена полей источника и назначения отличаются, как во французском объекте Personne Этот раздел решает другую проблему.

Что если объект данных, который мы демаршируем, представляет поле даты и времени, такое как long или Unix time, например:

1182882159000

Но наш собственный эквивалентный объект данных представляет то же поле даты и времени и значение в этом формате ISO, например String:

2007-06-26T21:22:39Z

Преобразователь по умолчанию просто отобразит длинное значение на String следующим образом:

"1182882159000"

Это определенно испортило бы наше приложение. Так как мы решаем это? Мы решаем это, добавляя блок конфигурации в XML-файл отображения и определяя наш собственный конвертер .

Сначала давайте скопируем Person DTO удаленного приложения с полем name, , затем датой и временем рождения, dtob :

public class Personne3 {
    private String name;
    private long dtob;

    public Personne3(String name, long dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

   //standard getters and setters
}

а вот наше собственное:

public class Person3 {
    private String name;
    private String dtob;

    public Person3(String name, String dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

   //standard getters and setters
}

Обратите внимание на разницу типов dtob в DTO источника и назначения.

Давайте также создадим наш собственный CustomConverter для передачи в Dozer в отображаемом XML:

public class MyCustomConvertor implements CustomConverter {
    @Override
    public Object convert(Object dest, Object source, Class<?> arg2, Class<?> arg3) {
        if (source == null)
            return null;

        if (source instanceof Personne3) {
            Personne3 person = (Personne3) source;
            Date date = new Date(person.getDtob());
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            String isoDate = format.format(date);
            return new Person3(person.getName(), isoDate);

        } else if (source instanceof Person3) {
            Person3 person = (Person3) source;
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            Date date = format.parse(person.getDtob());
            long timestamp = date.getTime();
            return new Personne3(person.getName(), timestamp);
        }
    }
}

Нам нужно только переопределить метод convert () , а затем вернуть все, что мы хотим вернуть. Нам помогают исходные и конечные объекты и их типы классов.

Обратите внимание, как мы позаботились о двунаправленности, предполагая, что источником может быть любой из двух классов, которые мы отображаем.

Мы создадим новый файл сопоставления для ясности, dozer custom convertor.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.baeldung.dozer.MyCustomConvertor">
                <class-a>com.baeldung.dozer.Personne3</class-a>
                <class-b>com.baeldung.dozer.Person3</class-b>
            </converter>
        </custom-converters>
    </configuration>
</mappings>

Это обычный файл отображения, который мы видели в предыдущих разделах, мы только добавили блок <configuration> </configuration> , внутри которого мы можем определить столько пользовательских преобразователей, сколько нам нужно, с их соответствующими исходными и целевыми классами данных.

Давайте проверим наш новый код CustomConverter :

@Test
public void givenSrcAndDestWithDifferentFieldTypes__whenAbleToCustomConvert__
  thenCorrect() {

    configureMapper("dozer__custom__convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Person3 person = new Person3("Rich", dateTime);
    Personne3 person0 = mapper.map(person, Personne3.class);

    assertEquals(timestamp, person0.getDtob());
}

Мы также можем проверить, является ли он двунаправленным:

@Test
public void givenSrcAndDestWithDifferentFieldTypes__
  whenAbleToCustomConvertBidirectionally__thenCorrect() {
    configureMapper("dozer__custom__convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Personne3 person = new Personne3("Rich", timestamp);
    Person3 person0 = mapper.map(person, Person3.class);

    assertEquals(dateTime, person0.getDtob());
}

10. Заключение

В этом руководстве мы познакомили вас с большинством основ библиотеки Dozer Mapping и с тем, как ее использовать в наших приложениях.

Полная реализация всех этих примеров и фрагментов кода может быть найдена в проекте Dozer github project .