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 .