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

от автора

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *