Introduction to MapStruct: An Easy and Fast Mapping at Compile Time
MapStruct: easy mappings between Java beans
Join the DZone community and get the full member experience.
Join For FreeIn a previous article, we learned about the proposal of DTO, the benefits and the issues about this new layer in our Java Application We can isolate the model from control in the MVC architecture perspective, although we add a new layer that implies more complexity, furthermore, there is a work of conversion between entity and DTO. In this tutorial, we'll explore one more tool the MapStruct.
MapStruct is a code generator that dramatically simplifies the mappings between Java bean types based on a convention over configuration approach. The generated mapping code uses explicit method invocations and thus is fast, type-safe, and easy to understand.
To demonstrate how to use this mapper and compare the ModelMapper with MapStruct, we'll rewrite the same application, however, with MapStruct instead. Thus, we'll create a MicroProfile/Jakarta EE application with Payara Micro to store the user information; on this service, we'll store the nickname, salary, languages, birthday, and several settings.
We'll use a maven project, therefore, we need to add the MapStruct in your dependencies as the first step.
xxxxxxxxxx
<build>
<finalName>microprofile</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compile.version}</version>
<configuration>
<target>${maven.compiler.target}</target>
<source>${maven.compiler.source}</source>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>
-Amapstruct.suppressGeneratorTimestamp=true
</compilerArg>
<compilerArg>
-Amapstruct.suppressGeneratorVersionInfoComment=true
</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</build>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
In the persistence layer, we'll use MongoDB that is so far the most popular NoSQL database in the globe. To make the communication more natural between Java and the database, we'll use Jakarta NoSQL. We'll define default settings to run locally, but thanks to Eclipse MicroProfile, we can overwrite those on the production environment.
xxxxxxxxxx
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Convert;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import my.company.infrastructure.MonetaryAmountAttributeConverter;
import javax.money.MonetaryAmount;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class User {
private String nickname;
MonetaryAmountAttributeConverter.class) (
private MonetaryAmount salary;
private List<String> languages;
private LocalDate birthday;
private Map<String, String> settings;
//getter and setter
}
import java.util.List;
import java.util.Map;
public class UserDTO {
private String nickname;
private String salary;
private List<String> languages;
private String birthday;
private Map<String, String> settings;
//getter and setter
}
The Magic happens in the interface that has the Mapper annotation, which marks the interface as a mapping interface and lets the MapStruct processor kick in during compilation. We set the component as CDI because we'll make the interface eligible to CDI context injection.
xxxxxxxxxx
import org.javamoney.moneta.Money;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import javax.money.MonetaryAmount;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
componentModel = "cdi") (
public interface UserMapper {
source = "salary", target = "salary", qualifiedByName = "moneyString") (
source = "birthday", target = "birthday", qualifiedByName = "localDateString") (
source = "languages", target = "languages", qualifiedByName = "languages") (
UserDTO toDTO(User user);
source = "salary", target = "salary", qualifiedByName = "money") (
source = "birthday", target = "birthday", qualifiedByName = "localDate") (
source = "languages", target = "languages", qualifiedByName = "languages") (
User toEntity(UserDTO dto);
"money") (
static MonetaryAmount money(String money) {
if (money == null) {
return null;
}
return Money.parse(money);
}
"localDate") (
static LocalDate localDate(String date) {
if (date == null) {
return null;
}
return LocalDate.parse(date);
}
"moneyString") (
static String moneyString(MonetaryAmount money) {
if (money == null) {
return null;
}
return money.toString();
}
"localDateString") (
static String localDateString(LocalDate date) {
if (date == null) {
return null;
}
return date.toString();
}
"languages") (
static List<String> convert(List<String> languages) {
if (languages == null) {
return Collections.emptyList();
}
return languages.stream().collect(Collectors.toList());
}
}
One of the significant advantages of using Jakarta NoSQL is its ease of integrating the database. For example, in this article, we will use the concept of a repository from which we will create an interface for which Jakarta NoSQL will take care of this implementation.
xxxxxxxxxx
import jakarta.nosql.mapping.Repository;
import javax.enterprise.context.ApplicationScoped;
import java.util.stream.Stream;
public interface UserRepository extends Repository<User, String> {
Stream<User> findAll();
}
In the last step, we will make our appeal with JAX-RS. The critical point is that the data exposure will all be done from the DTO; that is, it is possible to carry out any modification within the entity without the customer knowing, thanks to the DTO. As mentioned, the mapper was injected, and the 'map' method greatly facilitates this integration between the DTO and the entity without much code for that.
xxxxxxxxxx
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
"users") (
MediaType.APPLICATION_JSON) (
MediaType.APPLICATION_JSON) (
public class UserResource {
private UserRepository repository;
private UserMapper mapper;
public List<UserDTO> getAll() {
Stream<User> users = repository.findAll();
return users.map(mapper::toDTO)
.collect(Collectors.toList());
}
"{id}") (
public UserDTO findById( ("id") String id) {
return repository.findById(id)
.map(mapper::toDTO)
.orElseThrow(
() -> new WebApplicationException(Response.Status.NOT_FOUND));
}
public void insert(UserDTO dto) {
User map = mapper.toEntity(dto);
repository.save(map);
}
"{id}") (
public void update( ("id") String id, UserDTO dto) {
User user = repository.findById(id).orElseThrow(() ->
new WebApplicationException(Response.Status.NOT_FOUND));
repository.save(mapper.toEntity(dto));
}
"{id}") (
public void delete( ("id") String id) {
repository.deleteById(id);
}
}
The application is ready to run locally; we have either the docker image or install manually option to run a MongoDB instance and have fun.
In this tutorial, we'll move our application in the next step, moving it to the cloud, thanks to Platform.sh, where we need three files:
1) One Yaml files that have the application descriptions, such as the language, language version, who to build an application, and finally, how to execute it.
xxxxxxxxxx
name app
type"java:11"
disk1024
hooks
build mvn clean package payara-micro bundle
relationships
mongodb'mongodb:mongodb'
web
commands
start java -jar $JAVA_OPTS $CREDENTIAL target/microprofile-microbundle.jar --port $PORT
2) One to determine the services that your application need, yeap, don't worry about how to handle and maintain it, Platform.sh handles that to us.
xxxxxxxxxx
mongodb
type mongodb3.6
disk1024
3) The last you explain the routes
xxxxxxxxxx
"https://{default}/":
type upstream
upstream"app:http"
"https://www.{default}/"
type redirect
to"https://{default}/"
It is easy, isn't it? MapStruct allows a unique interface to create an optimized mapper in the Java world. It does not use reflections, therefore, it won't have the same issues that brings with reflection use. Software Architecture is about to take and understand the trade-off in any kind of situation.
Opinions expressed by DZone contributors are their own.
Comments