вступление
BULL (Bean Utils Light Library) — это преобразователь bean-компонента Java в bean-компонент, который рекурсивно копирует данные из одного объекта в другой. Это универсальный, гибкий, многоразовый, настраиваемый и невероятно быстрый.
Это единственная библиотека, способная преобразовывать изменяемые, неизменяемые и смешанные компоненты без какой-либо пользовательской конфигурации.
В этой статье объясняется, как его использовать, с конкретным примером для каждой доступной функции.
1. Зависимости
<dependency>
<groupId>com.hotels.beans</groupId>
<artifactId>bean-utils-library</artifactId>
<version>1.4.2</version>
</dependency>
Проект предоставляет две разные сборки , одну совместимую с jdk 8
(или выше) и одну с jdk 11
или выше.
Последнюю версию библиотеки можно получить из файла README или из CHANGELOG (если вам нужна jdk 8-
совместимая версия, обратитесь к CHANGELOG-JDK8 ).
2. Особенности
Функции макроса, описанные в этой статье:
- Преобразование Бина
- Проверка бина
3. Преобразование Бина
Преобразование компонента выполняется Transformer
объектом, который можно получить, выполнив следующую инструкцию:
Transformer transformer = new BeanUtils().getTransformer();
Получив Transformer
экземпляр объекта, мы можем использовать метод transform
для копирования нашего объекта в другой.
Используемый метод: K transform(T sourceObj, Class<K> targetObject);
где первый параметр представляет исходный объект, а второй — класс назначения.
Например, учитывая исходный и целевой класс:
public class FromBean { public class ToBean {
private final String name; public BigInteger id;
private final BigInteger id; private final String name;
private final List<FromSubBean> subBeanList; private final List<String> list;
private List<String> list; private final List<ImmutableToSubFoo> nestedObjectList;
private final FromSubBean subObject; private ImmutableToSubFoo nestedObject;
// all args constructor // constructors
// getters and setters... // getters and setters
} }
Преобразование можно получить с помощью следующей строки кода:
ToBean toBean = new BeanUtils().getTransformer().transform(fromBean, ToBean.class);
Обратите внимание, что порядок полей не имеет значения.
Копирование разных имен полей
Даны два класса с одинаковым количеством полей, но разными именами:
public class FromBean { public class ToBean {
private final String name; private final String differentName;
private final int id; private final int id;
private final List<FromSubBean> subBeanList; private final List<ToSubBean> subBeanList;
private final List<String> list; private final List<String> list;
private final FromSubBean subObject; private final ToSubBean subObject;
// all constructors // all args constructor
// getters... // getters...
} }
Нам нужно определить правильные отображения полей и передать их Transformer
объекту:
// the first parameter is the field name in the source object
// the second one is the the field name in the destination one
FieldMapping fieldMapping = new FieldMapping("name", "differentName");
Tansformer transformer = new BeanUtils().getTransformer().withFieldMapping(fieldMapping);
Затем мы можем выполнить преобразование:
ToBean toBean = transformer.transform(fromBean, ToBean.class);
Поля карты между исходным и целевым объектами
Случай 1: значение поля назначения должно быть получено из вложенного класса в исходном объекте
Предполагая, что объект FromSubBean
объявлен следующим образом:
public class FromSubBean {
private String serialNumber;
private Date creationDate;
// getters and setters...
}
и наш исходный класс и целевой класс описаны следующим образом:
public class FromBean { public class ToBean {
private final int id; private final int id;
private final String name; private final String name;
private final FromSubBean subObject; private final String serialNumber;
private final Date creationDate;
// all args constructor // all args constructor
// getters... // getters...
} }
… и что значения для полей serialNumber
и creationDate
в ToBean
объект необходимо извлечь subObject
, это можно сделать, определив полный путь к разделенному свойству свойству точки:
FieldMapping serialNumberMapping = new FieldMapping("subObject.serialNumber", "serialNumber");
FieldMapping creationDateMapping = new FieldMapping("subObject.creationDate", "creationDate");
ToBean toBean = new BeanUtils().getTransformer()
.withFieldMapping(serialNumberMapping, creationDateMapping)
.transform(fromBean, ToBean.class);
Случай 2: значение поля назначения (во вложенном классе) должно быть получено из корня исходного класса
В предыдущем примере показано, как получить значение из исходного объекта; вместо этого он объясняет, как поместить значение во вложенный объект.
Дано:
public class FromBean { public class ToBean {
private final String name; private final String name;
private final FromSubBean nestedObject; private final ToSubBean nestedObject;
private final int x;
// all args constructor // all args constructor
// getters... // getters...
} }
И:
public class ToSubBean {
private final int x;
// all args constructor
} // getters...
Предполагая, что значение x
должно быть отображено в поле: С учетом того, что x
содержится в ToSubBean
объекте, отображение поля должно быть определено следующим образом:
FieldMapping fieldMapping = new FieldMapping("x", "nestedObject.x");
Затем нам просто нужно передать его Transformer
и выполнить преобразование:
ToBean toBean = new BeanUtils().getTransformer()
.withFieldMapping(fieldMapping)
.transform(fromBean, ToBean.class);
Различные имена полей, определяющие аргументы конструктора
Сопоставление между различными полями также можно определить, добавив @ConstructorArg
аннотацию рядом с аргументами конструктора.
В @ConstructorArg
качестве входных данных берется имя соответствующего поля в исходном объекте.
public class FromBean { public class ToBean {
private final String name; private final String differentName;
private final int id; private final int id;
private final List<FromSubBean> subBeanList; private final List<ToSubBean> subBeanList;
private final List<String> list; private final List<String> list;
private final FromSubBean subObject; private final ToSubBean subObject;
// all args constructor
// getters...
public ToBean(@ConstructorArg("name") final String differentName,
@ConstructorArg("id") final int id,
} @ConstructorArg("subBeanList") final List<ToSubBean> subBeanList,
@ConstructorArg(fieldName ="list") final List<String> list,
@ConstructorArg("subObject") final ToSubBean subObject) {
this.differentName = differentName;
this.id = id;
this.subBeanList = subBeanList;
this.list = list;
this.subObject = subObject;
}
// getters...
}
Потом:
ToBean toBean = beanUtils.getTransformer().transform(fromBean, ToBean.class);
Применить пользовательское преобразование к лямбда-функции определенного поля
Мы знаем, что в реальной жизни редко, когда нам просто нужно скопировать информацию между двумя Java-компонентами, практически идентичными, часто случается так:
- Целевой объект имеет совершенно другую структуру, чем исходный объект
- Нам нужно выполнить некоторую операцию с определенным значением поля перед его копированием.
- Поля целевого объекта должны быть проверены
- Объект назначения имеет дополнительное поле, чем исходный объект, который должен быть заполнен чем-то, поступающим из другого источника
BULL дает возможность выполнять любые операции над определенным полем, фактически используя лямбда-выражения , разработчик может определить свой собственный метод, который будет применен к значению перед его копированием.
Давайте лучше объясним это на примере:
Учитывая следующий Source
класс:
public class FromFoo {
private final String id;
private final String val;
private final List<FromSubFoo> nestedObjectList;
// all args constructor
// getters
}
И следующий Destination
класс:
public class MixedToFoo {
public String id;
@NotNull
private final Double val;
// constructors
// getters and setters
}
И предполагая, что val
поле должно быть умножено на случайное значение в нашем преобразователе, у нас есть две проблемы:
val
Поле имеет другой тип , чемSource
объект, на самом деле это одинString
и одинDouble
- Нам нужно проинструктировать библиотеку о том, как мы будем применять математические операции
Ну, это довольно просто, вам просто нужно определить собственное лямбда-выражение, чтобы сделать это:
FieldTransformer<String, Double> valTransformer =
new FieldTransformer<>("val",
n -> Double.valueOf(n) * Math.random());
Выражение будет применено к полю с именем val
в целевом объекте.
Последний шаг — передать функции Transformer
экземпляр:
MixedToFoo mixedToFoo = new BeanUtils().getTransformer()
.withFieldTransformer(valTransformer)
.transform(fromFoo, MixedToFoo.class);
Назначьте значение по умолчанию в случае отсутствия поля в исходном объекте
Иногда это происходит, когда целевой объект имеет больше полей, чем исходный объект; в этом случае BeanUtils
библиотека выдаст исключение, сообщающее ей, что они не могут выполнить сопоставление, поскольку они не знают, откуда должно быть получено значение.
Типичный сценарий следующий:
public class FromBean { public class ToBean {
private final String name; @NotNull
private final BigInteger id; public BigInteger id;
private final String name;
private String notExistingField; // this will be null and no exceptions will be raised
// constructors... // constructors...
// getters... // getters and setters...
} }
Однако мы можем настроить библиотеку, чтобы назначить значение по умолчанию для типа поля (например, 0
для int
типа, null
для String
и т. Д.)
ToBean toBean = new BeanUtils().getTransformer()
.setDefaultValueForMissingField(true)
.transform(fromBean, ToBean.class);
Применение функции преобразования в случае отсутствия полей в исходном объекте
В приведенном ниже примере показано, как назначить значение по умолчанию (или результат лямбда-функции) для несуществующего поля в исходном объекте:
public class FromBean { public class ToBean {
private final String name; @NotNull
private final BigInteger id; public BigInteger id;
private final String name;
private String notExistingField; // this will have value: sampleVal
// all args constructor // constructors...
// getters... // getters and setters...
} }
Нам нужно назначить FieldTransformer
функцию для определенного поля:
FieldTransformer<String, String> notExistingFieldTransformer =
new FieldTransformer<>("notExistingField", () -> "sampleVal");
Вышеприведенные функции будут присваивать полю фиксированное значение notExistingField
, но мы можем возвращать все, что угодно, например, мы можем вызвать внешний метод, который возвращает значение, полученное после набора операций, что-то вроде:
FieldTransformer<String, String> notExistingFieldTransformer =
new FieldTransformer<>("notExistingField", () -> calculateValue());
Однако, в конце концов, нам просто нужно передать его в Transformer
.
ToBean toBean = new BeanUtils().getTransformer()
.withFieldTransformer(notExistingFieldTransformer)
.transform(fromBean, ToBean.class);
Применение функции преобразования к определенному полю во вложенном объекте
Случай 1: функция лямбда-преобразования применяется к определенному полю во вложенном классе
Дано:
public class FromBean { public class ToBean {
private final String name; private final String name;
private final FromSubBean nestedObject; private final ToSubBean nestedObject;
// all args constructor // all args constructor
// getters... // getters...
} }
И:
public class FromSubBean { public class ToSubBean {
private final String name; private final String name;
private final long index; private final long index;
// all args constructor // all args constructor
// getters... // getters...
} }
Предполагая, что функция лямбда-преобразования должна применяться только к полю, name
содержащемуся в ToSubBean
объекте, функция преобразования должна быть определена следующим образом:
FieldTransformer<String, String> nameTransformer = new FieldTransformer<>("nestedObject.name", StringUtils::capitalize);
Затем передайте функцию Transformer
объекту:
ToBean toBean = new BeanUtils().getTransformer()
.withFieldTransformer(nameTransformer)
.transform(fromBean, ToBean.class);
Случай 2: функция лямбда-преобразования применяется к определенному полю независимо от его местоположения
Представьте, что в нашем Destination
классе есть больше вхождений поля с одинаковым именем, расположенного в разных классах, и что мы хотим применить одну и ту же функцию преобразования ко всем из них; есть настройка, которая позволяет это.
Взяв в качестве примера вышеуказанные объекты и предположив, что мы хотим использовать все значения, содержащиеся в name
поле, независимо от их местоположения, мы можем сделать следующее:
FieldTransformer<String, String> nameTransformer = new FieldTransformer<>("name", StringUtils::capitalize);
Потом:
ToBean toBean = beanUtils.getTransformer()
.setFlatFieldTransformation(true)
.withFieldTransformer(nameTransformer)
.transform(fromBean, ToBean.class);
Функция статического трансформатора:
BeanUtils
предлагает «статическую» версию метода преобразователя, которая может быть добавленной стоимостью, когда ее необходимо применить в составном лямбда-выражении.
Например:
List<FromFooSimple> fromFooSimpleList = Arrays.asList(fromFooSimple, fromFooSimple);
Преобразование должно было быть сделано следующим:
Transformer transformer = new BeanUtils().getTransformer();
List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
.map(fromFoo -> transformer.transform(fromFoo, ImmutableToFooSimple.class))
.collect(Collectors.toList());
Благодаря этой возможности можно создать функцию преобразователя, специфичную для данного класса объектов:
Function<FromFooSimple, ImmutableToFooSimple> transformerFunction = BeanUtils.getTransformer(ImmutableToFooSimple.class);
Затем список можно преобразовать следующим образом:
List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
.map(transformerFunction)
.collect(Collectors.toList());
Однако может случиться так, что мы настроили Transformer
экземпляр с несколькими полями, функциями отображения и преобразования, и мы хотим использовать его также для этого преобразования, поэтому нам нужно создать функцию преобразователя из нашего преобразователя:
Transformer transformer = new BeanUtils().getTransformer()
.withFieldMapping(new FieldMapping("a", "b"))
.withFieldMapping(new FieldMapping("c", "d"))
.withTransformerFunction(new FieldTransformer<>("locale", Locale::forLanguageTag));
Function<FromFooSimple, ImmutableToFooSimple> transformerFunction = BeanUtils.getTransformer(transformer, ImmutableToFooSimple.class);
List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
.map(transformerFunction)
.collect(Collectors.toList());
Включить проверку Java Bean
Одной из функций, предлагаемых библиотекой, является проверка бина. Он состоит из проверки того, что преобразованный объект соответствует ограничениям, определенным для него. Проверка работает как со стандартным javax.constraints, так и с пользовательским.
Предполагая, что поле id
в FromBean
экземпляре равно null
.
public class FromBean { public class ToBean {
private final String name; @NotNull
private final BigInteger id; public BigInteger id;
private final String name;
// all args constructor // all args constructor
// getters... // getters and setters...
} }
При добавлении следующей конфигурации проверка будет выполнена в конце процесса преобразования, и в нашем примере будет сгенерировано исключение, сообщающее, что объект недействителен:
ToBean toBean = new BeanUtils().getTransformer()
.setValidationEnabled(true)
.transform(fromBean, ToBean.class);
Копировать в существующий экземпляр
Даже если библиотека способна создать новый экземпляр данного класса и заполнить его значениями в данном объекте, могут быть случаи, когда необходимо внедрить значения в уже существующий экземпляр, поэтому с учетом следующих компонентов Java Beans :
public class FromBean { public class ToBean {
private final String name; private String name;
private final FromSubBean nestedObject; private ToSubBean nestedObject;
// all args constructor // constructor
// getters... // getters and setters...
} }
Если нам нужно выполнить копирование для уже существующего объекта, нам просто нужно передать экземпляр класса в transform
функцию:
ToBean toBean = new ToBean();
new BeanUtils().getTransformer().transform(fromBean, toBean);
Пропустить преобразование на заданном наборе полей
В случае, если мы копируем значения исходного объекта в уже существующий экземпляр (с некоторыми уже установленными значениями), нам может потребоваться избежать того, чтобы операция преобразования переопределяла существующие значения. Пример ниже объясняет, как это сделать, учитывая:
public class FromBean { public class ToBean {
private final String name; private String name;
private final FromSubBean nestedObject; private ToSubBean nestedObject;
// all args constructor // constructor
// getters... // getters and setters...
} }
public class FromBean2 {
private final int index;
private final FromSubBean nestedObject;
// all args constructor
// getters...
}
Если нам нужно пропустить преобразование для набора полей, нам просто нужно передать их имя skipTransformationForField
методу. Например, если мы хотим пропустить преобразование на поле nestedObject
, это то, что нам нужно сделать:
ToBean toBean = new ToBean();
new BeanUtils().getTransformer()
.skipTransformationForField("nestedObject")
.transform(fromBean, toBean);
Эта функция позволяет трансформировать объект, сохраняя данные из разных источников .
Чтобы лучше объяснить эту функцию, давайте предположим, что ToBean
(определенный выше) должен быть преобразован следующим образом:
name
значение поля взято изFromBean
объектаnestedObject
значение поля взято изFromBean2
объекта
Цель может быть достигнута путем:
// create the destination object
ToBean toBean = new ToBean();
// execute the first transformation skipping the copy of: 'nestedObject' field that should come from the other source object
new BeanUtils().getTransformer()
.skipTransformationForField("nestedObject")
.transform(fromBean, toBean);
// then execute the transformation skipping the copy of: 'name' field that should come from the other source object
new BeanUtils().getTransformer()
.skipTransformationForField("name")
.transform(fromBean2, toBean);
Преобразование типов полей
В случае, когда тип поля отличается от исходного класса и места назначения, у нас есть этот пример:
public class FromBean { public class ToBean {
private final String index; private int index;
// all args constructor // constructor
// getters... // getters and setters...
} }
Его можно преобразовать с помощью специальной функции преобразования:
FieldTransformer<String, Integer> indexTransformer = new FieldTransformer<>("index", Integer::parseInt);
ToBean toBean = new BeanUtils()
.withFieldTransformer(indexTransformer)
.transform(fromBean, ToBean.class);
4. Проверка бобов
Проверка класса по набору правил может быть очень полезна, особенно когда нам нужно убедиться, что данные объекта соответствуют нашим ожиданиям.
Аспект «проверки поля» — это одна из функций, предлагаемых BULL, и она полностью автоматическая — вам нужно только аннотировать ваше поле одной из существующих javax.validation.constraints (или определить пользовательскую) и затем выполнить проверку для этого ,
Учитывая следующий боб:
public class SampleBean {
@NotNull
private BigInteger id;
private String name;
// constructor
// getters and setters...
}
Экземпляр вышеуказанного объекта:
SampleBean sampleBean = new SampleBean();
И одна строка кода, такая как:
new BeanUtils().getValidator().validate(sampleBean);
Это будет бросаться InvalidBeanException
, как поле id
является null
.
Вывод
Я попытался объяснить — на примерах — как использовать основные функции, предлагаемые проектом BULL. Тем не менее, просмотр полного исходного кода может быть даже более полезным.
Дополнительные примеры можно найти, посмотрев тестовые примеры, реализованные в проекте BULL, доступные здесь .
GitHub также содержит пример проекта Spring Boot, который использует библиотеку для преобразования объектов запроса / ответа между различными слоями, которые можно найти здесь .