«1. Обзор

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

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

Вот некоторые способы добиться этого: жестко закодировать логику копирования или реализовать средства сопоставления компонентов, такие как Dozer. Однако его можно использовать для упрощения процесса сопоставления между одним слоем объектов и другим.

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

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

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

Мы создаем объект MapperFactory следующим образом:

MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

Затем предположим, что у нас есть исходный объект данных, Source.java, с двумя полями:

public class Source {
    private String name;
    private int age;
    
    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(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // standard getters and setters
}

Это самый простой способ сопоставления бинов с помощью Orika:

@Test
public void givenSrcAndDest_whenMaps_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class);
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source("Baeldung", 10);
    Dest dest = mapper.map(src, Dest.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

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

@Test
public void givenSrcAndDest_whenMapsReverse_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest("Baeldung", 10);
    Source dest = mapper.map(src, Source.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

3. Настройка Maven

Чтобы использовать Orika mapper в наших проектах maven, нам нужно иметь зависимость orika-core в pom.xml:

<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.4.6</version>
</dependency>

~~ ~ Последнюю версию всегда можно найти здесь.

3. Работа с MapperFactory

Общий шаблон сопоставления с Orika включает в себя создание объекта MapperFactory, его настройку на случай, если нам потребуется настроить поведение сопоставления по умолчанию, получение из него объекта MapperFacade и, наконец, фактическое сопоставление.

Мы будем наблюдать эту закономерность во всех наших примерах. Но наш самый первый пример показал поведение картографа по умолчанию без каких-либо настроек с нашей стороны.

3.1. BoundMapperFacade против MapperFacade

Следует отметить, что мы могли бы использовать BoundMapperFacade вместо MapperFacade по умолчанию, который довольно медленный. Это случаи, когда у нас есть определенная пара типов для сопоставления.

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

@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
    BoundMapperFacade<Source, Dest> 
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Source src = new Source("baeldung", 10);
    Dest dest = boundMapper.map(src);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

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

@Test
public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() {
    BoundMapperFacade<Source, Dest> 
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Dest src = new Dest("baeldung", 10);
    Source dest = boundMapper.mapReverse(src);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

В противном случае тест завершится неудачно.

3.2. Настройка сопоставления полей

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

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

public class Person {
    private String name;
    private String nickname;
    private int age;
    
    public Person(String name, String nickname, int age) {
        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(String nom, String surnom, int age) {
        this.nom = nom;
        this.surnom = surnom;
        this.age = age;
    }
    
    // standard getters and setters
}

Орика не может автоматически разрешить эти различия. Но мы можем использовать ClassMapBuilder API для регистрации этих уникальных сопоставлений.

Мы уже использовали его раньше, но еще не использовали ни одну из его мощных функций. Первая строка каждого из наших предыдущих тестов с использованием MapperFacade по умолчанию использовала ClassMapBuilder API для регистрации двух классов, которые мы хотели отобразить:

mapperFactory.classMap(Source.class, Dest.class);

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

mapperFactory.classMap(Source.class, Dest.class).byDefault()

Добавляя вызов метода byDefault(), мы уже настраиваем поведение преобразователя с помощью API ClassMapBuilder.

Теперь мы хотим иметь возможность сопоставлять Personne с Person, поэтому мы также настраиваем сопоставления полей с помощью ClassMapBuilder API:

@Test
public void givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect() {
    mapperFactory.classMap(Personne.class, Person.class)
      .field("nom", "name").field("surnom", "nickname")
      .field("age", "age").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Personne frenchPerson = new Personne("Claire", "cla", 25);
    Person englishPerson = mapper.map(frenchPerson, Person.class);

    assertEquals(englishPerson.getName(), frenchPerson.getNom());
    assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
    assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}

Не забудьте вызвать метод API register() для регистрации конфигурация с помощью MapperFactory.

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

Это скоро станет утомительным, что, если мы хотим сопоставить только одно поле из 20, нужно ли нам настраивать все их сопоставления?

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

mapperFactory.classMap(Personne.class, Person.class)
  .field("nom", "name").field("surnom", "nickname").byDefault().register();

Здесь мы не определили отображение для поля возраста, но, тем не менее, тест пройдет.

3.3. Исключить поле

Предположим, что мы хотим исключить поле nom объекта Personne из сопоставления, чтобы объект Person получал новые значения только для неисключенных полей:

@Test
public void givenSrcAndDest_whenCanExcludeField_thenCorrect() {
    mapperFactory.classMap(Personne.class, Person.class).exclude("nom")
      .field("surnom", "nickname").field("age", "age").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Personne frenchPerson = new Personne("Claire", "cla", 25);
    Person englishPerson = mapper.map(frenchPerson, Person.class);

    assertEquals(null, englishPerson.getName());
    assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
    assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}

Обратите внимание, как мы его исключаем в конфигурации MapperFactory, а затем обратите внимание на первое утверждение, в котором мы ожидаем, что значение name в объекте Person останется нулевым в результате его исключения из сопоставления.

4. Отображение коллекций

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

4.1. Списки и массивы

Рассмотрим исходный объект данных, который имеет только одно поле, список имен людей:

public class PersonNameList {
    private List<String> nameList;
    
    public PersonNameList(List<String> nameList) {
        this.nameList = nameList;
    }
}

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

public class PersonNameParts {
    private String firstName;
    private String lastName;

    public PersonNameParts(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

~~ ~ Предположим, мы абсолютно уверены, что в индексе 0 всегда будет имя человека, а в индексе 1 всегда будет его фамилия.

Orika позволяет нам использовать запись в квадратных скобках для доступа к членам коллекции:

@Test
public void givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonNameList.class, PersonNameParts.class)
      .field("nameList[0]", "firstName")
      .field("nameList[1]", "lastName").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    List<String> nameList = Arrays.asList(new String[] { "Sylvester", "Stallone" });
    PersonNameList src = new PersonNameList(nameList);
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Sylvester");
    assertEquals(dest.getLastName(), "Stallone");
}

Даже если вместо PersonNameList у нас был PersonNameArray, тот же тест прошел бы для массива имен.

4.2. Карты

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

Точно так же мы знаем, что в той же карте есть еще один ключ, last, значение которого представляет собой lastName человека в целевом объекте.

public class PersonNameMap {
    private Map<String, String> nameMap;

    public PersonNameMap(Map<String, String> nameMap) {
        this.nameMap = nameMap;
    }
}

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

Орика допускает два способа получения ключа, оба представлены в следующем тесте:

@Test
public void givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonNameMap.class, PersonNameParts.class)
      .field("nameMap['first']", "firstName")
      .field("nameMap[\"last\"]", "lastName")
      .register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Map<String, String> nameMap = new HashMap<>();
    nameMap.put("first", "Leornado");
    nameMap.put("last", "DiCaprio");
    PersonNameMap src = new PersonNameMap(nameMap);
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Leornado");
    assertEquals(dest.getLastName(), "DiCaprio");
}

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

5. Сопоставление вложенных полей

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

public class PersonContainer {
    private Name name;
    
    public PersonContainer(Name name) {
        this.name = name;
    }
}
public class Name {
    private String firstName;
    private String lastName;
    
    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

@Test
public void givenSrcWithNestedFields_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonContainer.class, PersonNameParts.class)
      .field("name.firstName", "firstName")
      .field("name.lastName", "lastName").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    PersonContainer src = new PersonContainer(new Name("Nick", "Canon"));
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Nick");
    assertEquals(dest.getLastName(), "Canon");
}

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

6. Сопоставление нулевых значений

@Test
public void givenSrcWithNullField_whenMapsThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = mapper.map(src, Dest.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

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

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

6.1. Глобальная конфигурация

MapperFactory mapperFactory = new DefaultMapperFactory.Builder()
  .mapNulls(false).build();

Мы можем настроить наш преобразователь для сопоставления нулей или игнорировать их на глобальном уровне перед созданием глобального MapperFactory. Помните, как мы создали этот объект в нашем самом первом примере? На этот раз мы добавим дополнительный вызов в процессе сборки:

@Test
public void givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class);
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

Мы можем запустить тест, чтобы убедиться, что нули действительно не сопоставляются:

Что происходит, так это то, что по умолчанию нули нанесен на карту. Это означает, что даже если значение поля в исходном объекте равно null, а соответствующее значение поля в целевом объекте имеет значимое значение, оно будет перезаписано.

В нашем случае поле назначения не перезаписывается, если соответствующее поле источника имеет нулевое значение.

6.2. Локальная конфигурация

«Отображение нулевых значений можно контролировать в ClassMapBuilder с помощью mapNulls(true|false) или mapNullsInReverse(true|false) для управления отображением нулевых значений в обратном направлении.

При установке этого значения в экземпляре ClassMapBuilder все сопоставления полей, созданные в том же ClassMapBuilder, после установки значения примут это же значение.

@Test
public void givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .mapNulls(false).field("name", "name").byDefault().register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

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

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

@Test
public void givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest(null, 10);
    Source dest = new Source("Vin", 44);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

Двунаправленное отображение также принимает отображаемые нулевые значения:

@Test
public void 
  givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .mapNullsInReverse(false).field("name", "name").byDefault()
      .register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest(null, 10);
    Source dest = new Source("Vin", 44);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Vin");
}

Также мы можем предотвратить это, вызвав mapNullsInReverse и передав значение false:

6.3. Конфигурация на уровне поля

mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
  .fieldMap("name", "name").mapNulls(false).add().byDefault().register();

Мы можем настроить это на уровне поля с помощью fieldMap, например:

@Test
public void givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .fieldMap("name", "name").mapNulls(false).add().byDefault().register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

В этом случае конфигурация повлияет только на поле имени, как мы назвали его на уровне поля:

7. Пользовательское сопоставление Orika

До сих пор мы рассматривали простые примеры пользовательского сопоставления с использованием API ClassMapBuilder. Мы по-прежнему будем использовать тот же API, но настроим наше сопоставление с помощью класса CustomMapper от Orika.

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

2007-06-26T21:22:39Z

Один объект данных представляет это значение как строку даты и времени в следующем формате ISO:

1182882159000

, а другой представляет то же самое, что и тип long в следующем формате временной метки unix:

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

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

Давайте создадим наш первый объект данных:

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

затем наш второй объект данных:

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

class PersonCustomMapper extends CustomMapper<Personne3, Person3> {

    @Override
    public void mapAtoB(Personne3 a, Person3 b, MappingContext context) {
        Date date = new Date(a.getDtob());
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        String isoDate = format.format(date);
        b.setDtob(isoDate);
    }

    @Override
    public void mapBtoA(Person3 b, Personne3 a, MappingContext context) {
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        Date date = format.parse(b.getDtob());
        long timestamp = date.getTime();
        a.setDtob(timestamp);
    }
};

Вот наша конкретная реализация абстрактного класса CustomMapper:

Обратите внимание, что мы реализовали методы mapAtoB и mapBtoA. Реализация обоих делает нашу функцию отображения двунаправленной.

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

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

@Test
public void givenSrcAndDest_whenCustomMapperWorks_thenCorrect() {
    mapperFactory.classMap(Personne3.class, Person3.class)
      .customize(customMapper).register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Personne3 personne3 = new Personne3("Leornardo", timestamp);
    Person3 person3 = mapper.map(personne3, Person3.class);

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

Давайте запустим тест, чтобы убедиться, что наш пользовательский преобразователь работает:

Обратите внимание, что мы по-прежнему передаем пользовательский преобразователь в преобразователь Orika через API ClassMapBuilder, как и все другие простые настройки.

@Test
public void givenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect() {
    mapperFactory.classMap(Personne3.class, Person3.class)
      .customize(customMapper).register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Person3 person3 = new Person3("Leornardo", dateTime);
    Personne3 personne3 = mapper.map(person3, Personne3.class);

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

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

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

В этой статье мы рассмотрели наиболее важные особенности структуры отображения Orika.

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