With the release of Spring Boot 3, integrating MapStruct, a popular Java-based code generator, becomes even more streamlined, offering developers a powerful tool for converting between different object models seamlessly.
Eang Sopheaktra
August 18 2024 08:09 pm
The fully fledged server uses the following:
There are a number of third-party dependencies used in the project. Browse the Maven pom.xml file for details of libraries and versions used.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<annotationProcessorPaths combine.children="append">
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruck-bindings.version}</version>
</path>
<path>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>6.5.2.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
You will need:
Clone the project and use Maven to build the server
mvn clean install
Using Data Transfer Objects (DTOs) instead of directly returning entities has several advantages, especially in the context of modern software architectures:
Separation of Concerns:
Security:
Decoupling:
Performance Optimization:
Flexibility:
Validation:
Versioning:
After understanding about why we should using dto and then our fetching from db is entity data so we are sure to use mapper instead of manual writing mapper.
MapStruct is a compile-time code generator that creates type-safe, performant, and easy-to-maintain mappers. Unlike other mapping frameworks that rely on reflection at runtime, MapStruct generates plain Java code, ensuring minimal overhead and maximum performance.
I created AbstractUserMapper for example with mapper as abstract class and ability when we are using as abstract class for mapping.
package com.tra21.mapstruct_example.mappers;
import com.tra21.mapstruct_example.models.User;
import com.tra21.mapstruct_example.payloads.dtos.requests.users.UserCreateRequestDto;
import com.tra21.mapstruct_example.payloads.dtos.requests.users.UserUpdateRequestDto;
import com.tra21.mapstruct_example.payloads.enums.UserStatus;
import com.tra21.mapstruct_example.repositories.IUserRepository;
import org.mapstruct.AfterMapping;
import org.mapstruct.BeanMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.util.Optional;
@Mapper(componentModel = "spring", imports = { UserStatus.class, LocalDateTime.class }, uses = { IUserMapper.class })
public abstract class AbstractUserMapper {
@Autowired
protected IUserRepository userRepository;
@Mappings({
@Mapping(target = "id", ignore = true),
@Mapping(target = "status", expression = "java(UserStatus.ACTIVE)"),
@Mapping(target = "createdDate", expression = "java(LocalDateTime.now())"),
@Mapping(target = "createdBy", ignore = true),
@Mapping(target = "lastModifiedDate", ignore = true),
@Mapping(target = "lastModifiedBy", ignore = true)
})
public abstract User mapCreate(UserCreateRequestDto userCreateRequestDto);
@BeanMapping(ignoreByDefault = true)
@Mappings({
@Mapping(target = "firstName", source = "firstName"),
@Mapping(target = "middleName", source = "middleName"),
@Mapping(target = "lastName", source = "lastName"),
@Mapping(target = "email", source = "email"),
@Mapping(target = "birthDate", source = "birthDate"),
@Mapping(target = "lastModifiedDate", expression = "java(LocalDateTime.now())"),
@Mapping(target = "lastModifiedBy", ignore = true)
})
public abstract User mapUpdate(@MappingTarget User user, UserUpdateRequestDto userUpdateRequestDto);
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "status", source = "status", qualifiedByName = "deleteUser")
public abstract User mapDelete(User user);
@BeanMapping(ignoreByDefault = true)
@Mapping(target = "status", source = ".")
public abstract User mapUpdateUserStatus(@MappingTarget User user, UserStatus userStatus);
@Named("deleteUser")
protected UserStatus deleteUser(UserStatus userStatus){
return userStatus.equals(UserStatus.DELETED) ? userStatus : UserStatus.DELETED;
}
//just example before mapping
@BeforeMapping
protected void mapUserCreateBefore(UserCreateRequestDto userCreateRequestDto, @MappingTarget User user){
Optional<User> userOptional = userRepository.findById(1L);
userOptional.ifPresent(user::setCreatedBy);
}
//just example after mapping
@AfterMapping
protected void mapUserUpdateAfter(UserUpdateRequestDto userUpdateRequestDto, @MappingTarget User user){
Optional<User> userOptional = userRepository.findById(1L);
userOptional.ifPresent(user::setLastModifiedBy);
}
}
In above abstract class is using:
@Mapper(componentModel = "spring", imports = { UserStatus.class, LocalDateTime.class },
uses = { IUserMapper.class })
@Autowired
or constructor injection.imports = { UserStatus.class, LocalDateTime.class }: will be import UserStatus and LocalDateTime class in our mapping
@BeanMapping(ignoreByDefault = true): will be ignore by default and need to assign by yourself
@Named("deleteUser"): will be using for @Mapping with qualifiedByName = "deleteUser"
@BeforeMapping: will be using before our map is do
@AfterMapping: will be using after success map.
@Mapping(target = "lastModifiedDate", expression = "java(LocalDateTime.now())"):
you can also using expression for set value to mapping field.
@Mapping(target = "id", ignore = true): you also can ignore field what you don't want to map.
After abstract mapper I'm just trying to example about interface of mapping also
package com.tra21.mapstruct_example.mappers;
import com.tra21.mapstruct_example.models.User;
import com.tra21.mapstruct_example.payloads.dtos.responses.PaginationResponseDto;
import com.tra21.mapstruct_example.payloads.dtos.responses.users.UserResponseDto;
import com.tra21.mapstruct_example.utils.DateUtils;
import com.tra21.mapstruct_example.utils.UserUtils;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.slf4j.helpers.MessageFormatter;
import org.springframework.data.domain.Page;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Optional;
@Mapper(componentModel = "spring", imports = { UserUtils.class, LocalDateTime.class })
public interface IUserMapper {
@Mappings({
@Mapping(target = "data", source = "content"),
@Mapping(target = "page", source = "pageable.pageNumber"),
@Mapping(target = "size", source = "pageable.pageSize"),
@Mapping(target = "totalElements", source = "totalElements"),
@Mapping(target = "totalPages", source = "totalPages")
})
PaginationResponseDto<UserResponseDto> mapUserPagination(Page<User> userPage);
@Mappings({
@Mapping(target = "userId", source = "id"),
@Mapping(target = "age", expression = "java(UserUtils.getAge(user.getBirthDate()))"),
@Mapping(target = "fullName", source = ".", qualifiedByName = "getFullName"),
@Mapping(target = "updatedBy", source = "lastModifiedBy", qualifiedByName = "optionalUserMap"),
@Mapping(target = "createdBy", source = "createdBy", qualifiedByName = "optionalUserMap"),
@Mapping(target = "createdAt", ignore = true),
@Mapping(target = "updatedAt", ignore = true)
})
UserResponseDto mapUserResponse(User user);
@AfterMapping
default void afterUserMapping(User user, @MappingTarget UserResponseDto userResponseDto){
if(user.getCreatedDate().isPresent()){
userResponseDto.setCreatedAt(DateUtils.convertLocalDateToDate(user.getCreatedDate().get()));
}
if(user.getLastModifiedDate().isPresent()){
userResponseDto.setUpdatedAt(DateUtils.convertLocalDateToDate(user.getLastModifiedDate().get()));
}
}
@Named("getFullName")
default String getFullName(User user){
if(StringUtils.hasText(user.getMiddleName())){
return MessageFormatter.arrayFormat(
"{} {} {}",
new Object[]{
user.getLastName(),
user.getMiddleName(),
user.getFirstName()
}
).getMessage();
}
return MessageFormatter.arrayFormat(
"{} {}",
new Object[]{
user.getLastName(),
user.getFirstName()
}
).getMessage();
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Named("optionalUserMap")
default String optionalUserMap(Optional<User> userOptional){
return userOptional.map(this::getFullName).orElse(null);
}
}
Download the source code for the sample application with Mapstruct for mapping object data. After this tutorial you will learn such as: