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

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

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

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

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

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

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

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

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

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

Теперь, когда у нас есть общее представление о том, как работает 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 — это String , а points — это double , тогда как в целевом классе id и points — это integer s.

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

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

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

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

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

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

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

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

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

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

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

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

public Person() {}

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

// standard getters and setters
}

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

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

public Personne() {}

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

// standard getters and setters
}

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

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

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

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

Наконец, обратите внимание, что мы не включили возраст поля в наш пользовательский файл сопоставления. Французское слово «возраст» по-прежнему «возраст», что подводит нас к еще одной важной особенности 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.foreach.dozer.Personne</class-a>
<class-b>com.foreach.dozer.Person</class-b>
<field>
<a>nom</a>
<b>name</b>
</field>
<field>
<a>surnom</a>
<b>nickname</b>
</field>
</mapping>
</mappings>

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

По умолчанию подстановочный знак имеет значение 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);
}

Как мы видим в последнем утверждении, поле age назначения осталось равным 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() , который мы должны переопределить, чтобы определить наши конфигурации. Затем, точно так же, как наши теги <mapping></mapping> в XML, мы определяем столько TypeMappingBuilder , сколько нам требуется.

Эти построители сообщают 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 человека удаленного приложения с именем, затем датой и временем рождения, полем 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.foreach.dozer.MyCustomConvertor">
<class-a>com.foreach.dozer.Personne3</class-a>
<class-b>com.foreach.dozer.Person3</class-b>
</converter>
</custom-converters>
</configuration>
</mappings>

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

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

@Test
public void givenSrcAndDestWithDifferentFieldTypes_whenAbleToCustomConvert_
thenCorrect() {

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

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

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

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

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

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

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

Полную реализацию всех этих примеров и фрагментов кода можно найти в проекте Dozer на github .