Как стать автором
Обновить

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

Время на прочтение14 мин
Количество просмотров2K
Автор оригинала: Fabio Borriello

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, мы можем использовать метод transform, чтобы скопировать наш объект в другой.

Используемый метод: K transform(T sourceObj, Class<K> targetObject); где первый параметр представляет исходный объект, а второй - целевой класс.

Пример исходного и целевого класса:

public class FromBean {                              public class ToBean {                           
   private final String name;                            public BigInteger id;                                  
   private final BigInteger id;                          private final String name;                             
   private final List<FromSubBean> subBeanList;          private final List<String> list;                       
   private List<String> list;                            private final List<ImmutableToSubFoo> nestedObjectList;
   private final FromSubBean subObject;                  private ImmutableToSubFoo nestedObject;                

    // all args constructor                              // constructors                                         
   // getters and setters...                             // getters and setters
}                                                    }

Преобразование можно выполнить с помощью следующей строки кода:

ToBean toBean = new BeanUtils().getTransformer().transform(fromBean, ToBean.class);

Обратите внимание, что порядок полей не имеет значения

Копирование полей с разными именами

Даны два класса с одинаковым количеством полей, но разными именами:

Нам нужно определить правильные сопоставления полей и передать их объекту 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 проекта, который использует библиотеку для преобразования объектов запроса/ответа между различными уровнями, который можно найти здесь.

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии5

Публикации

Истории

Работа

Java разработчик
350 вакансий

Ближайшие события