By this post I'll be starting a series of posts dedicated to different code generators, which makes our lives easier, and more fun. As we don't need to write boring code!
Did you ever find yourself in a situation where you needed to copy data from one Java bean to another? Or maybe from an Entity to DTO? Does next code look familiar to you ?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dto.setAttribute1( entity.getAttribute1() ); | |
dto.setAttribute2( entity.getAttribute2() ); | |
dto.setAttribute3( entity.getAttribute3() ); | |
dto.setAttribute4( entity.getAttribute4() ); | |
dto.setAttribute5( entity.getAttribute5() ); | |
/* ... a few more pairs of set/get lines omitted */ | |
dto.setAttribute21( entity.getAttribute21() ); | |
dto.setAttribute22( entity.getAttribute22() ); | |
dto.setAttribute23( entity.getAttribute23() ); | |
dto.setAttribute24( entity.getAttribute24() ); | |
dto.setAttribute25( entity.getAttribute25() ); |
For demo purposes of this post let's start with a simple example. Assume we have an @Embeddable AddressEntity class, as part of our domain model. And we'd like to create Address POJOs from this embeddable.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Embeddable | |
public class AddressEntity implements Serializable { | |
private String street; | |
private String city; | |
public AddressEntity() { | |
} | |
/* getters/setters */ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Address { | |
private String street; | |
private String city; | |
public Address() { | |
} | |
public Address(String street, String city) { | |
this.street = street; | |
this.city = city; | |
} | |
/* getters/setters */ | |
} |
We could have done it with the help of a constructor:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public Address(AddressEntity entity) { | |
if (entity == null) { | |
throw new IllegalArgumentException("Entity cannot be null"); | |
} | |
this.street = entity.getStreet(); | |
this.city = entity.getCity(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static Address from(AddressEntity entity) { | |
if (entity == null) { | |
throw new IllegalArgumentException("Entity cannot be null"); | |
} | |
return new Address(entity.getStreet(), entity.getCity()); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Mapper | |
public interface AddressMapper { | |
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class); | |
Address toAddress(AddressEntity entity); | |
} |
Let's look closer at what should be done to create a mapper. First we need to create an interface (called AddressMapper in our case) and put @Mapper annotation on it. This annotation is a marker annotation for MapStruct. It'll find all mappers and generate the implementation for them. If you'd like you can make mappers from abstract classes as well, but personally I prefer interfaces.
As we would like to access our mapper somehow - INSTANCE interface member is defined. To get the generated mapper we need to call Mappers.getMapper( ... ).
Now how this would work on practice and when this code generation will happen? As it's an Annotation Processor based code generator we should include it in our build process. In case of Maven project it can be done by specifying annotation processor in Maven's compiler plugin like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<plugin> | |
<groupId>org.apache.maven.plugins</groupId> | |
<artifactId>maven-compiler-plugin</artifactId> | |
<version>3.5.1</version> | |
<configuration> | |
<source>${maven.compiler.source}</source> | |
<target>${maven.compiler.target}</target> | |
<annotationProcessorPaths> | |
<path> | |
<groupId>org.mapstruct</groupId> | |
<artifactId>mapstruct-processor</artifactId> | |
<version>${org.mapstruct.version}</version> | |
</path> | |
</annotationProcessorPaths> | |
</configuration> | |
</plugin> |
Let's consider UserEntity and User pair and create a mapper for them:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
public class UserEntity implements Serializable { | |
public static enum Gender { | |
MALE, FEMALE | |
} | |
@Id | |
private Long id; | |
private String userName; | |
private String password; | |
private String firstName; | |
private String lastName; | |
private int age; | |
private AddressEntity address; | |
private Gender gender; | |
private LocalDate lastVisit; | |
/* setters/getters/constructors */ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class User { | |
private String firstName; | |
private String lastName; | |
private Credentials credentials; | |
private Address address; | |
private int age; | |
private String gender; | |
private LocalDate visit; | |
/* setters/getters/constructors */ | |
} | |
public class Credentials { | |
private String userName; | |
private String password; | |
/* setters/getters/constructors */ | |
} |
Here's a possible way to define a mapper for such type pair:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Mapper(uses = AddressMapper.class) | |
public interface UserMapper { | |
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); | |
@Mapping(source = "lastVisit", target = "visit") | |
@Mapping(source = "userName", target = "credentials.userName") | |
@Mapping(source = "password", target = "credentials.password") | |
User toUser(UserEntity entity); | |
default String genderToString(UserEntity.Gender gender) { | |
return gender.equals(UserEntity.Gender.FEMALE) ? "f" : "m"; | |
} | |
} |
And here's a test to make sure that everything works as expected:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void toUserTest() { | |
UserEntity entity = Samples.userEntity(); | |
User user = UserMapper.INSTANCE.toUser(entity); | |
assertThat(user) | |
.isNotNull() | |
.hasFieldOrPropertyWithValue("firstName", entity.getFirstName()) | |
.hasFieldOrPropertyWithValue("lastName", entity.getLastName()) | |
.hasFieldOrPropertyWithValue("gender", "m") | |
.hasFieldOrPropertyWithValue("age", entity.getAge()) | |
.hasFieldOrPropertyWithValue("visit", entity.getLastVisit()); | |
assertThat(user.getCredentials()) | |
.isNotNull() | |
.hasFieldOrPropertyWithValue("userName", entity.getUserName()) | |
.hasFieldOrPropertyWithValue("password", entity.getPassword()); | |
assertAddress(entity.getAddress(), user.getAddress()); | |
} |
All the examples with tests and configurations can be found in this GitHub repository.
To be continued...
COMMENTS