Hello, my dear friends and readers! Before we write an article, a little background ... Recently I encountered one problem in working with the Mapstruct library , which I briefly described in my telegram channel here . In the comments, the problem for the record was solved, my colleague from the previous project helped with this. After that, I decided to write an article on this topic, but of course we will not look narrowly and will first try to get in the know, understand what Mapstruct is and why it is needed, and already on a real example we will analyze the situation that arose earlier and how to solve it. Therefore, I strongly recommend doing all the calculations in parallel with reading the article in order to experience everything in practice. Before starting - subscribe to my telegram channel, I collect my activities there, write thoughts about development in Java and IT in general. Subscribed? Great! Well, now let's go!
Mapstruct, faq?
A code generator for fast type-safe bean mappings. Our first task is to figure out what Mapstruct is and why we need it. In general, you can read about it on the official website. On the main page of the site there are three answers to the questions: what is it? For what? How? Let's try to do this:
What it is?
Mapstruct is a library that helps to map (map, in general, they always say: map, map, etc.) objects of some entities into objects of other entities using generated code based on configurations that are described through interfaces.
For what?
For the most part, we develop multi-layer applications (the database layer, the business logic layer, the application interaction layer with the outside world) and each layer has its own objects for storing and processing data. And this data needs to be transferred from layer to layer by transferring from one entity to another. For those who have not worked with this approach, it may seem somewhat complicated. For example, we have an entity for the Student database. When the data of this entity goes into the business logic (services) layer, we need to translate the data from the Student class to the StudentModel class. Further, after all the manipulations with the business logic, the data must be given out. And for this we have the StudentDto class. Of course, we need to pass data from the StudentModel class to StudentDto. Writing by hand every time methods that will be transferred is laborious. Plus, this is extra code in the codebase that needs to be maintained. You can make a mistake. And Mapstruct generates such methods at the compilation stage and stores them in generated-sources.
How?
With the help of annotations. We just need to create an annotation that will have the main Mapper annotation, which will tell the library that the methods in this interface can be used to translate from one object to another. As I said earlier about students, in our case it will be the StudentMapper interface, which will have several methods for transferring data from one layer to another:
The beauty of this approach is that if the names and types of fields are the same in different classes (as in our case), then the settings for Mapstruct are enough to generate the necessary implementation based on the StudentMapper interface at the compilation stage, which will translate. It's clearer now, right? Let's go further and use a real example to analyze the work in a Spring Boot application.
An example of how Spring Boot and Mapstruct work
The first thing we need is to create a Spring Boot project and add Mapstruct to it. For this case, I have an organization in GitHub with templates for repositories and start for Spring Boot is one of them. Based on it, we create a new project: Next, we get the project . Yes, friends, put a star on the project if you found it useful, so I will know that I'm doing it for a reason. In this project, we will reveal the situation that I received at work and described in a post on my telegram channel. I will briefly outline the situation for those who are not in the know: when we write tests for mappers (that is, for those interface implementations that we talked about earlier), we want the tests to pass as quickly as possible. The simplest option with mappers is to use the SpringBootTest annotation during test startup, which will raise the entire ApplicationContext of the Spring Boot application and inject the mapper needed for the test inside the test. But this option is resource-intensive and takes much more time, so it is not suitable for us. We need to write a unit test that simply creates the desired mapper and checks that its methods work exactly as we expect. Why do you need to run tests faster? If the tests take a long time, then it slows down the entire development process. Until the tests pass on the new code, this code cannot be considered correct and it will not be taken into testing, which means it will not be taken into production, which means that the developer did not complete the work. It would seem, why write a test for a library whose work is beyond doubt? And yet, we need to write a test, because we are testing how correctly the mapper was described and whether it does what we expect. First of all, to make things easier for us, let's add Lombok to our project by adding another dependency to pom.xml:
In our project, we will need to translate from model classes (which are used to work with business logic) to DTO classes that we use to communicate with the outside world. In our simplified version, we will assume that the fields do not change and our mappers will be simple. But, if there is a desire, it will be possible to write a more detailed article on how to work with Mapstruct, how to configure it, how to use its advantages. But then, since this article will come out rather big. Let's say we have a student with a list of lectures and lecturers that he attends. Let's create a model package . Based on this, we will create a simple model:
Now let's create a mapper that will translate the collection of lecture models into a collection of DTO lectures. The first thing to do is to add Mapstruct to the project. To do this, use their official website , everything is described there. That is, we need to add one dependency and a plugin to our pomnik (if you have questions about what a pomnik is, here you go Article1 and Article2 ):
It should be noted separately that in mappers we refer to other mappers. This is done through the uses field in the Mapper annotation, as is done in StudentMapper:
Here we use two mappers to correctly map the list of lectures and the list of lecturers. Now we need to compile our code and see what's there and how. You can do this with the mvn clean compile command . But, as it turned out, when creating implementations for the Mapstruct of our mappers, the mapper implementations did not overwrite the fields. Why? It turned out that it was not possible to pick up the Data annotation from Lombok. And something had to be done ... Therefore, we have a new section in the article.
Connecting Lombok and Mapstruct
After several minutes of searching, it turned out that Lombok and Mapstruct needed to be connected in a certain way. The Mapstruct documentation has information on this . After examining the example that the developers from Mapstruct offered, let's update our pom.xml: Let's add versions separately:
After that, everything should work out. Let's compile our project again. But where to look for the classes generated by Mapstruct? They're in generated-sources: ${projectDir}/target/generated-sources/annotations/ Now that we're prepared to deal with my frustration in the Mapstruct post, let's try to create tests for mappers.
Writing tests for our mappers
I'll create a quick and simple test that would test one of the mappers in case we create an integration test and don't care about the time it takes:
Here, with the SpringBootTest annotation, we launch the entire applicationContext and extract the class we need for testing from it using the Autowired annotation. From the point of view of speed and ease of writing a test, this is very good. The test passed successfully, all is well. But we will go the other way and write a unit test for a mapper, for example, on LectureListMapper ...
Since the implementations that Mapstruct generates are in the same class as our project, we can safely use them in our tests. Everything looks great - no annotations, we create the class that we need in the simplest way and that's it. But when we run the test, we will understand that it will fail and there will be a NullPointerException in the console ... This is because the implementation of the LectureListMapper mapper looks like this:
If we look, then NPE (short for NullPointerException), we get just from the lectureMapper variable, which is not initialized. But our implementation does not have a constructor with which we could initialize the variable. This is exactly the reason why Mapstruct implemented the mapper in this way! In Spring, there are several ways to add beans to classes, you can inject them through a field along with the Autowired annotation, as done above, or you can inject them through a constructor. In such a problematic situation, I found myself at work when it was necessary to optimize the execution time of tests. I thought that nothing could be done about it and poured out my pain on my telegram channel. And then they helped me in the comments, they said that it is possible to set up an injection strategy. The Mapper interface has a field injectionStrategy , which just accepts the enam InjectionStrategywhich has two values: FIELD and CONSTRUCTOR . Now, knowing this, let's add this setting to our mappers, I'll show it on the example of LectureListMapper :
I highlighted in bold the part that I added. Let's add this option for everyone else and recompile the project so that mappers are generated already with a new line. When this is done, let's see how the implementation of the mapper for LectureListMapper has changed (highlighted in bold the part that we need):
And now Mapstruct has implemented mapper injection through the constructor. Exactly what we were looking for. Now our test will stop compiling, let's update it and get:
Now, if we run the test, everything will work as it should, because in the LectureListMapperImpl we pass the LectureMapper it needs... Victory! It’s not difficult for you, but I’m pleased: Friends, everything is as usual, subscribe to my github account , to the telegram account . There I post the result of my activities, there are really useful things) I especially invite you to join the discussion group of the telegram channel . It so happened that if someone has a technical question, you can get an answer there. This format is interesting for everyone, you can read who knows what and gain experience.
Conclusion
As part of this article, we got acquainted with such a necessary and often used product as Mapstruct. Understand what it is, why and how. On a real example, we felt what can be done and how it can be changed. Also, we figured out how to set up bean injection through the constructor, so that it would be possible to test mappers normally. Colleagues from Mapstruct allowed users of their product to choose how to inject mappers, for which we are undoubtedly grateful. BUT, despite the fact that Spring recommends injecting beans through the constructor, the guys from Mapstruct set the default to inject through the field. Why is that? No answer. I suspect there may be reasons we just don't know about and that's why they did it. And to learn from them, I created a GitHub issuein their official product repository.
GO TO FULL VERSION