Перейти к основному содержимому

Картирование с Орикой

· 14 мин. чтения

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("ForEach", 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("ForEach", 10);
Source dest = mapper.map(src, Source.class);

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

3. Настройка Мавена

Чтобы использовать картограф Orika в наших проектах 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. The BoundMapperFacade против MapperFacade

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

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

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

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

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

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

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

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

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

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

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

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 по умолчанию использовала API ClassMapBuilder для регистрации двух классов, которые мы хотели отобразить:

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();

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

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, а затем обратите внимание на первое утверждение, в котором мы ожидаем, что значение имени в объекте 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. Карты

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

Точно так же мы знаем, что в той же карте есть еще один ключ, 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;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@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. Пользовательское картирование Орика

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

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

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

2007-06-26T21:22:39Z

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

1182882159000

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

Вот наша конкретная реализация абстрактного класса 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);
}
};

Обратите внимание, что мы реализовали методы 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 через ClassMapBuilder API, как и все другие простые настройки.

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

@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 .

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

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