Сопоставление с Орикой

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

1. обзор

Orika - это структура отображения Java Bean, котораяrecursively copies data from one object to another. Это может быть очень полезно при разработке многослойных приложений.

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

Вот несколько способов добиться этого:hard coding the copying logic or to implement bean mappers like Dozer. Тем не менее, его можно использовать для упрощения процесса отображения между одним слоем объекта и другим.

Orikauses byte code generation to create fast mappers с минимальными накладными расходами, что делает его намного быстрее, чем другие преобразователи на основе отражения, такие как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("example", 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("example", 10);
    Source dest = mapper.map(src, Source.class);

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

3. Maven Setup

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


    ma.glasnost.orika
    orika-core
    1.4.6

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

3. Работа сMapperFactory

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

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

3.1. BoundMapperFacade противMapperFacade

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

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

@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
    BoundMapperFacade
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Source src = new Source("example", 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
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Dest src = new Dest("example", 10);
    Source dest = boundMapper.mapReverse(src);

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

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

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

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

Рассмотрим исходный объектPerson с тремя полями, а именноname,nickname и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
}

Орика не может автоматически разрешить эти различия. Но мы можем использоватьClassMapBuilderAPI to register these unique mappings.

Мы уже использовали его раньше, но пока не задействовали ни одну из его мощных функций. В первой строке каждого из наших предыдущих тестов с использованием значения по умолчаниюMapperFacade использовалсяClassMapBuilderAPI to register the two classes we wanted to map:

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

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

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

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

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

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

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

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

Это скоро станет утомительным,what if we only want to map one field out of 20, нужно ли нам настраивать все их сопоставления?

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

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

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

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

Предполагая, что мы хотели бы исключить полеnomPersonne из сопоставления, чтобы объект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 останетсяnull, как в результате исключения из отображения.

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

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

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

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

public class PersonNameList {
    private List nameList;

    public PersonNameList(List nameList) {
        this.nameList = nameList;
    }
}

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

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

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

Предположим, мы очень уверены, что в индексе 0 всегда будетfirstName человека, а в индексе 1 всегда будет ихlastName.

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

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

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

public class PersonNameMap {
    private Map nameMap;

    public PersonNameMap(Map 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 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 и соответствующее значение поля в целевом объекте имеет значимое значение, оно будет перезаписано.

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

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

Отображением значенийnull можно управлять на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 непосредственно перед регистрацией поляname, это приведет к тому, что все поля, следующие за вызовомmapNulls, будут проигнорированы, если они имеют значениеnull.

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

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

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

@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

До сих пор мы рассмотрели простые примеры настраиваемого сопоставления с использованиемClassMapBuilderAPI. We shall still use the same API but customize our mapping using Orika’s CustomMapper class.

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

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

2007-06-26T21:22:39Z

а другой представляет то же самое, что и типlong в следующем формате временной метки 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 {

    @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. Реализация обоих делает нашу картографическую функцию двунаправленной.

Each method exposes the data objects we are mapping and we take care of copying the field values from one to the other.

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

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

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

Обратите внимание, что мы по-прежнему передаем настраиваемый картограф в картограф Орики черезClassMapBuilderAPI, just like all other simple customizations.

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

@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. Заключение

В этой статье у нас естьexplored the most important features of the Orika mapping framework.

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

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