У циклі статей про популярний генератор коду Mapstruct будемо розбирати його можливості для спрощення роботи під час написання програмних продуктів.
MapStruct підтримує мета анотації, тому в цій статті поговоримо про те, яким чином можна уніфікувати структуру наших мапперів, щоб не використовувати дублювання коду або повторне написання маппінгу для полів, які є спільними для декількох класів і dto.
Налаштування проекту pom.xml
В pom.xml я також використовую lombok, тому налаштування mapstruct в категорії plugins мають дещо спецефічні з метою безконфліктної робооти lombok і mapstruct. Інакше генерування коду для класу lombok не буде працювати.
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${org.slf4j.simple.version}</version>
</dependencyНалаштування проекту pom.xml>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.databind.version}</version>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${org.slf4j.simple.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.databind.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Припустимо в нашому проекті є класи, які мають декілька спільних полів. Нам потрібно описати маппер, який буде правильно співвідносити назви полів однієї сутності(model) і з класом трансфером(dto – data transfer object).
Для прикладу скористаємося наступними класами:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(of = "id")
public class BoxModel {
private Long id;
private LocalDate creationDate;
private String name;
private String label;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(of = "id")
public class ShelveModel {
private Long id;
private LocalDate creationDate;
private String name;
private String weightLimit;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ShelveDto {
private String id;
private String date;
private String groupName;
private String maxWeight;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoxDto {
private String id;
private String date;
private String groupName;
private String designation;
}
@Data, @Builder, @AllArgsConstructor, @NoArgsConstructor, @EqualsAndHashCode – це анотації від бібліотеки Project Lombok, дану бібліотеку ми не розг в цих статтях, звичайно можна використовувати підхід, де при створенні класу, розробник самостійно генерує всі конструктори, методи, які необхідні.
Як видно з класів BoxModel, ShelveModel в них є спільні поля (id, creationDate, name). Для уникнення дублювання коду, ми використаємо мождивості MapStruct і створимо власну анотацію, яка дозволить мати в одному місці зміни і використовувати їх для маппінгу наших класів таким чином, що не повторювати маппінг однакових полів і логіку роботи, якщо потрібно мати якесь специфічне значення в конкретному полі ( тут ми використаємо варіант із LocalDate.now() коли перемаплюємо об’єкт dto в model).
Додамо наступний код:
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", expression = "java(java.time.LocalDate.now())")
@Mapping(target = "name", source = "groupName")
public @interface ToModel {}
Створивши наступну анотацію, будемо використовувати в головному маппері для класів. Відразу визначаємо правила заповнення полів, які необхідні для класів. Тепер можемо написати основний маппер, який буде конвертувати значення отриманні з dto в model і навпаки. Як видно ми використовуємо власну анотацію @ToModel і анотацію @Mapping в якій вказуємо конкретні специфічні поля для кожної пари класів.
- значення для поля id – ігоноруємо, навіть якщо в dto поле буде присутнє і матиме значення;
- поле creationDate завжди заповнюємо значенням, яке повертає нам метод now() із LocalDate класу;
- groupName, яке в даному випадку завжди співпадає з полем name в моделі.
Тепер можемо створити маппер, який буде конвертувати значення отриманні з dto в model і навпаки. Як видно у маппері, використовуючи власну анотацію @ToModel і анотацію @Mapping, можна спростити написання коду, мати одне місце для маппінгу спільних полів, а також можливість маппити окремі поля за допомогою @Mapping.
@Mapper
public interface StorageMapper {
StorageMapper INSTANCE = Mappers.getMapper(StorageMapper.class);
@ToModel
@Mapping(target = "weightLimit", source = "maxWeight")
ShelveModel toModel(ShelveDto source);
@ToModel
@Mapping(target = "label", source = "designation")
BoxModel toModel(BoxDto source);
@ToDto
@Mapping(target = "designation", source = "label")
BoxDto toDto(BoxModel entity);
@ToDto
@Mapping(target = "maxWeight", source = "weightLimit")
ShelveDto toDto(ShelveModel entity);
}