Как преобразовать любой тип Java Bean с помощью BULL

Kate

Administrator
Команда форума
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 необходимо умножить на случайное значение в нашем трансформаторе, у нас есть две задачи:

  1. Поле val имеет тип, отличный от объекта Source, действительно, одно - String, а второе - Double.
  2. Нам нужно проинструктировать библиотеку о том, как мы будем применять математическую операцию
Что ж, это довольно просто, вам просто нужно определить собственное лямбда-выражение, чтобы сделать это:

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/
 
Сверху