BULL расшифровывается как Bean Utils Light Library, преобразователь, рекурсивно копирующий данные из одного объекта в другой.
Введение
BULL (Bean Utils Light Library) - это преобразователь Java-bean-bean-компонента в Java-bean, который рекурсивно копирует данные из одного объекта в другой. Он - универсальный, гибкий, многоразовый, настраиваемый и невероятно быстрый.
Это единственная библиотека, способная преобразовывать изменяемые, неизменяемые и смешанные bean-компоненты без какой-либо пользовательской конфигурации.
В этой статье объясняется, как его использовать, с конкретным примером для каждой функции.
1. Зависимости
<dependency>
<groupId>com.hotels.beans</groupId>
<artifactId>bull-bean-transformer</artifactId>
<version>2.0.1.1</version>
</dependency>
В проекте предусмотрены две разные сборки: одна совместима с jdk 8
(или выше), другая с поддержкой jdk 11
версии 2.0.0, jdk 15
и выше.
Последнюю доступную версию библиотеки можно узнать в файле README или в CHANGELOG (если вам нужна jdk 8-
совместимая версия, обратитесь к CHANGELOG-JDK8 ).
2. Функции
В этой статье описаны следующие функции макросов:
Преобразование бина
Валидация бина
3. Преобразование бина
Преобразование bean-компонента выполняется объектом Transformer
, который можно получить, выполнив следующий оператор:
BeanTransformer transformer = new BeanUtils().getTransformer();
Когда у нас есть экземпляр объекта BeanTransformer
, мы можем использовать преобразование метода, чтобы скопировать наш объект в другой.
Используемый метод: 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);
Обратите внимание, что порядок полей не имеет значения
Копирование полей с разными именами
Даны два класса с одинаковым количеством полей, но разными именами:
Нам нужно определить правильные сопоставления полей и передать их объекту Transformer
:
// первый параметр - это имя поля в исходном объекте
// второй - имя поля в целевом
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 дает возможность выполнять любые операции с определенным полем, фактически используя лямбда-выражения, разработчик может определить свой собственный метод, который будет применяться к значению перед его копированием.
Давайте лучше объясним это на примере, используя следующий исходный класс:
public class FromFoo {
private final String id;
private final String val;
private final List<FromSubFoo> nestedObjectList;
// all args constructor
// getters
}
и следующий целевой класс:
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: функция лямбда-преобразования, примененная к определенному полю независимо от его местоположения
Представьте, что в нашем целевом классе больше вхождений поля с тем же именем, расположенных в разных классах, и что мы хотим применить одну и ту же функцию преобразования ко всем из них; есть настройка, которая позволяет это.
Взяв, в качестве примера, возьмем указанные выше объекты и предполагая, что мы хотим все значения, содержащиеся в поле name ,
написамть прописными буквами, независимо от их местоположения, мы можем сделать следующее:
FieldTransformer<String, String> nameTransformer =
new FieldTransformer<>("name", StringUtils::capitalize);
затем:
ToBean toBean = beanUtils.getTransformer()
.setFlatFieldTransformation(true)
.withFieldTransformer(nameTransformer)
.transform(fromBean, ToBean.class);
Функция статического трансформера
BeanUtils
предлагает «статическую» версию метода transformer, который может дать дополнительные преимущества, когда его необходимо применить в составном лямбда-выражении.
Например:
List<FromFooSimple> fromFooSimpleList = Arrays.asList(fromFooSimple, fromFooSimple);
Преобразование должно было быть выполнено следующим образом:
BeanTransformer transformer = new BeanUtils().getTransformer();
List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
.map(fromFoo -> transformer.transform(fromFoo, ImmutableToFooSimple.class))
.collect(Collectors.toList());
Благодаря этой функции можно создать функцию transformer, специфичную для данного класса объектов:
Function<FromFooSimple, ImmutableToFooSimple> transformerFunction =
BeanUtils.getTransformer(ImmutableToFooSimple.class);
Тогда список можно преобразовать следующим образом:
List<ImmutableToFooSimple> actual = fromFooSimpleList.stream()
.map(transformerFunction)
.collect(Collectors.toList());
Однако может случиться так, что мы настроили экземпляр BeanTransformer
с несколькими полями, функциями отображенения и преобразования, и мы хотим использовать его также для этого преобразования, поэтому нам нужно создать функцию-преобразователь из нашего трансформера:
BeanTransformer 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
Одна из функций, предлагаемых библиотекой, - это валидация 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
Цель может быть достигнута, при выполнении:
// создать целевой объект
ToBean toBean = new ToBean();
// выполнить первое преобразование, пропуская копию поля: 'nestedObject',
// которое должно быть получено из другого исходного объекта
new BeanUtils().getTransformer()
.skipTransformationForField("nestedObject")
.transform(fromBean, toBean);
// затем выполните преобразование, пропуская копию поля: 'name',
// которое должно быть получено из другого исходного объекта
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);
Преобразование Java Bean с использованием шаблона Builder
Библиотека поддерживает преобразование Java Bean с использованием различных типов шаблонов Builder: стандартного (поддерживается по умолчанию) и пользовательского. Давайте посмотрим на них подробнее и как включить преобразование пользовательского типа Builder.
Начнем со стандартного, поддерживаемого по умолчанию:
public class ToBean {
private final Class<?> objectClass;
private final Class<?> genericClass;
ToBean(final Class<?> objectClass, final Class<?> genericClass) {
this.objectClass = objectClass;
this.genericClass = genericClass;
}
public static ToBeanBuilder builder() {
return new ToBean.ToBeanBuilder();
}
// getter methods
public static class ToBeanBuilder {
private Class<?> objectClass;
private Class<?> genericClass;
ToBeanBuilder() {
}
public ToBeanBuilder objectClass(final Class<?> objectClass) {
this.objectClass = objectClass;
return this;
}
public ToBeanBuilder genericClass(final Class<?> genericClass) {
this.genericClass = genericClass;
return this;
}
public com.hotels.transformer.model.ToBean build() {
return new ToBean(this.objectClass, this.genericClass);
}
}
}
Как уже говорилось, для этого не требуются дополнительные настройки, поэтому преобразование можно осуществить, выполнив:
ToBean toBean = new BeanTransformer()
.transform(sourceObject, ToBean.class);
Пользовательский шаблон Builder:
public class ToBean {
private final Class<?> objectClass;
private final Class<?> genericClass;
ToBean(final ToBeanBuilder builder) {
this.objectClass = builder.objectClass;
this.genericClass = builder.genericClass;
}
public static ToBeanBuilder builder() {
return new ToBean.ToBeanBuilder();
}
// getter methods
public static class ToBeanBuilder {
private Class<?> objectClass;
private Class<?> genericClass;
ToBeanBuilder() {
}
public ToBeanBuilder objectClass(final Class<?> objectClass) {
this.objectClass = objectClass;
return this;
}
public ToBeanBuilder genericClass(final Class<?> genericClass) {
this.genericClass = genericClass;
return this;
}
public com.hotels.transformer.model.ToBean build() {
return new ToBean(this);
}
}
}
Чтобы преобразовать вышеуказанный Bean компонент, используйте следующую инструкцию:
ToBean toBean = new BeanTransformer()
.setCustomBuilderTransformationEnabled(true)
.transform(sourceObject, ToBean.class);
Преобразование записей Java
Начиная с JDK 14 был представлен новый тип объектов: записи Java (Java Records). Записи - это неизменяемые классы данных, для которых требуется только типы и имена полей. Методы equals, hashCode и toString, а также закрытые, конечные поля и общедоступный конструктор генерируются компилятором Java.
Запись Java определяется следующим образом:
public record FromFooRecord(BigInteger id, String name) {
}
легко трансформируется в эту запись:
public record ToFooRecord(BigInteger id, String name) {
}
с помощью простой инструкции:
ToFooRecord toRecord = new BeanTransformer().transform(sourceRecord, ToFooRecord.class);
Библиотека также может преобразовывать из Record в Java Bean и наоборот.
4. Валидация Bean
Проверка класса на соответствие набору правил может быть очень полезной, особенно когда нам нужно убедиться, что данные объекта соответствуют нашим ожиданиям.
Аспект «валидация поля» - одна из функций, предлагаемых BULL, и она полностью автоматическая - вам нужно только аннотировать свое поле одним из существующих javax.validation.
constraints (или определить настраиваемый), а затем выполнить проверку этого правила.
Рассмотрим следующий bean-компонент:
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 проекта, который использует библиотеку для преобразования объектов запроса/ответа между различными уровнями, который можно найти здесь.