«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, который содержит значения для всех полей, которые имеют то же имя поля, что и Исходный объект.

В качестве альтернативы, вместо того, чтобы передавать 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

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

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

Последняя версия доступна здесь.

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 — это строка, а точки — двойное число, тогда как в целевом классе и id, и точки — целые числа.

Давайте теперь посмотрим, как 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, оба целых числа в месте назначения. объект.

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

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

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

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

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

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

У нас есть объект Person с полями имени, псевдонима и возраста:

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 для сопоставления объекта French 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, который у нас может быть.

«На данный момент достаточно заметить, что у нас есть \u003cmappings\u003e в качестве корневого элемента, у которого есть дочерний элемент \u003cmapping\u003e. У нас может быть столько дочерних элементов внутри \u003cmappings\u003e, сколько случаев пар классов, которым требуется пользовательское сопоставление.

Обратите также внимание на то, как мы указываем исходный и конечный классы внутри тегов \u003cmapping\u003e\u003c/mapping\u003e. За ним следует \u003cfield\u003e\u003c/field\u003e для каждой пары исходного и целевого полей, для которой требуется пользовательское сопоставление.

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

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

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

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

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.

Мы также можем загрузить пользовательский файл отображения из-за пределов пути к классам, если нам нужно, используйте префикс «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>

~ ~~ Обратите внимание, что мы добавили подстановочный знак атрибута к элементу \u003cmapping\u003e\u003c/mapping\u003e, которого раньше не было.

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

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

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

@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);
}

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

7. Пользовательское сопоставление через Аннотации

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

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

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

Чтобы реализовать это, нам нужно всего лишь добавить аннотацию @mapper(“destinationFieldName”) к методам получения в исходном объекте. Вот так:

@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(), который мы должны переопределить для определения наших конфигураций. Затем, точно так же, как наши теги \u003cmapping\u003e\u003c/mapping\u003e в XML, мы определяем столько TypeMappingBuilders, сколько нам нужно.

Эти компоновщики сообщают 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. Пользовательские преобразователи

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

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

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

1182882159000

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

2007-06-26T21:22:39Z

Конвертер по умолчанию просто сопоставит длинное значение со строкой следующим образом:

"1182882159000"

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

Во-первых, давайте реплицируем DTO Person удаленного приложения с именем, затем датой и временем рождения, полем 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>

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

Давайте протестируем наш новый код 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.