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 проекта, который использует библиотеку для преобразования объектов запроса/ответа между различными уровнями, который можно найти здесь.
ссылка на оригинал статьи https://habr.com/ru/post/565624/
Добавить комментарий