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("Baeldung", 10);
Dest dest = mapper.map(source, Dest.class);
assertEquals(dest.getName(), "Baeldung");
assertEquals(dest.getAge(), 10);
}
Как мы видим, после сопоставления Dozer результатом будет новый экземпляр объекта Dest , который содержит значения для всех полей, имена которых совпадают с именем объекта Source .
В качестве альтернативы, вместо передачи mapper в класс Dest , мы могли бы просто создать объект Dest и передать mapper его ссылку:
@Test
public void givenSourceObjectAndDestObject__whenMapsSameNameFieldsCorrectly__
thenCorrect() {
Source source = new Source("Baeldung", 10);
Dest dest = new Dest();
mapper.map(source, dest);
assertEquals(dest.getName(), "Baeldung");
assertEquals(dest.getAge(), 10);
}
3. Maven Setup
Теперь, когда у нас есть общее представление о том, как работает Dozer, давайте добавим следующую зависимость в pom.xml :
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.5.1</version>
</dependency>
Последняя версия доступна по ссылке here .
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, оба __integer __s в целевом объекте.
5. Основные пользовательские сопоставления через XML
Во всех предыдущих примерах, которые мы видели, объекты данных источника и назначения имеют одинаковые имена полей, что позволяет легко сопоставлять их с нашей стороны.
Однако в реальных приложениях будет множество случаев, когда два отображаемых нами объекта данных не будут иметь полей с общим именем свойства.
Чтобы решить эту проблему, Dozer дает нам возможность создать пользовательскую конфигурацию отображения в XML .
В этом XML-файле мы можем определить записи отображения классов, которые механизм отображения Dozer будет использовать, чтобы решить, какой атрибут источника сопоставить с каким атрибутом назначения.
Давайте рассмотрим пример и попробуем демаршировать объекты данных из приложения, созданного французским программистом, к английскому стилю именования наших объектов.
У нас есть объект Person с полями name , nickname и 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 для отображения французского объекта 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.baeldung.dozer.Personne</class-a>
<class-b>com.baeldung.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> для каждой пары полей источника и назначения, для которых требуется настраиваемое сопоставление.
Наконец, обратите внимание, что мы не включили поле age в наш файл пользовательского сопоставления. Французское слово для обозначения возраста - все еще возраст, что подводит нас к еще одной важной особенности Dozer.
-
Свойства, имеющие одно и то же имя, не нужно указывать в файле сопоставления XML ** . Dozer автоматически сопоставляет все поля с одинаковым именем свойства из исходного объекта в целевой объект.
Затем мы поместим наш пользовательский файл XML в путь к классам прямо в папке src . Однако, где бы мы ни помещали его в путь к классам, Dozer будет искать по всему пути к классам в поисках указанного файла.
Давайте создадим вспомогательный метод для добавления файлов сопоставления в наш mapper :
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-файл.
Мы также можем загрузить пользовательский файл сопоставления извне classpath, если нам нужно, использовать префикс « 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.baeldung.dozer.Personne</class-a>
<class-b>com.baeldung.dozer.Person</class-b>
<field>
<a>nom</a>
<b>name</b>
</field>
<field>
<a>surnom</a>
<b>nickname</b>
</field>
</mapping>
</mappings>
Обратите внимание, что мы добавили атрибут wildcard к элементу <mapping> </mapping> , которого раньше не было.
По умолчанию wildcard это true . Он сообщает движку Dozer, что мы хотим, чтобы все поля в исходном объекте были сопоставлены с соответствующими полями назначения.
Когда мы устанавливаем false, мы говорим Dozer только отображать поля, которые мы явно указали в XML.
Таким образом, в приведенной выше конфигурации мы хотим отобразить только два поля, оставив age :
@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») к методам getter в исходном объекте. Вот так:
@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 __s, сколько нам нужно.
Эти компоновщики сообщают 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. Пользовательские конвертеры
Другой сценарий, с которым мы можем столкнуться при отображении, - это то, где мы хотели бы выполнить пользовательское отображение между двумя объектами .
Мы рассмотрели сценарии, в которых имена полей источника и назначения отличаются, как во французском объекте Personne Этот раздел решает другую проблему.
Что если объект данных, который мы демаршируем, представляет поле даты и времени, такое как long или Unix time, например:
1182882159000
Но наш собственный эквивалентный объект данных представляет то же поле даты и времени и значение в этом формате ISO, например String:
2007-06-26T21:22:39Z
Преобразователь по умолчанию просто отобразит длинное значение на String следующим образом:
"1182882159000"
Это определенно испортило бы наше приложение. Так как мы решаем это? Мы решаем это, добавляя блок конфигурации в XML-файл отображения и определяя наш собственный конвертер .
Сначала давайте скопируем Person DTO удаленного приложения с полем name, , затем датой и временем рождения, 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.baeldung.dozer.MyCustomConvertor">
<class-a>com.baeldung.dozer.Personne3</class-a>
<class-b>com.baeldung.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 project .