Spring Boot 3 with MapStruct

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.

Sopheaktra

Eang Sopheaktra

August 18 2024 08:09 pm

0 409

Requirements

The fully fledged server uses the following:

  • Spring Framework
  • SpringBoot
  • Mapstruct
  • Lombok

Dependencies

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

<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>

Building the project

You will need:

  • Java JDK 17 or higher
  • Maven 3.5.1 or higher
  • Tomcat 10.1

Clone the project and use Maven to build the server

 mvn clean install

Before Understand About Mapper why we need dto instead of response back as entity?

Using Data Transfer Objects (DTOs) instead of directly returning entities has several advantages, especially in the context of modern software architectures:

  • Separation of Concerns:

    • DTOs allow you to separate your domain model (entities) from your API model. This separation ensures that changes in your database schema do not directly impact your API, providing a layer of abstraction.
  • Security:

    • Directly exposing entities can lead to unintentional data leakage. DTOs let you control exactly what data is exposed in your API responses, reducing the risk of exposing sensitive information.
  • Decoupling:

    • DTOs help decouple your internal data model from the external interface. This is particularly important if you need to evolve your API independently of your database schema.
  • Performance Optimization:

    • DTOs can be tailored to include only the necessary data for a particular API response, which can reduce payload size and improve performance. Entities might have fields that are not needed in the API response, leading to inefficient data transfer.
  • Flexibility:

    • DTOs can aggregate data from multiple entities or services, providing a more convenient and efficient data structure for the client. This avoids the need for the client to make multiple requests to gather related data.
  • Validation:

    • DTOs can be used to enforce validation rules specific to the API contract. This can be different from the validation rules applied at the database level.
  • Versioning:

    • DTOs make it easier to version your API. You can create different versions of DTOs without changing your underlying entities, allowing you to introduce changes without breaking existing clients.

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.

Why Use MapStruct?

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.

Key Benefits:

  1. Performance: Since MapStruct generates mapping code at compile time, it avoids the performance costs associated with reflection.
  2. Type Safety: Compile-time checks ensure that mappings are valid, reducing the likelihood of runtime errors.
  3. Reduced Boilerplate: MapStruct significantly cuts down the amount of manual mapping code, making your codebase cleaner and more maintainable.

Code

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 })

  • componentModel="spring" : will be registered as a Spring bean, and you can inject it into your Spring-managed components using @Autowired or constructor injection.
  • imports = { UserStatus.class, LocalDateTime.class }: will be import UserStatus and LocalDateTime class in our mapping

  • uses = { IUserMapper.class }: will be using existing IUserMapper behavior if our mapper meet same entity mapping on IUserMapper.
@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);
    }
}

Summary

Download the source code for the sample application with Mapstruct for mapping object data. After this tutorial you will learn such as:

  • Abstract Map with Mapstruct
  • Interface Map with Mapstruct

 

 

Comments

Subscribe to our newsletter.

Get updates about new articles, contents, coding tips and tricks.

Weekly articles
Always new release articles every week or weekly released.
No spam
No marketing, share and spam you different notification.
© 2023-2025 Tra21, Inc. All rights reserved.