文章目录
- 前言
- 一、pom依赖
- 二、简单使用
- 2.1 转换类型
- 2.1.1 Bean -> Bean
- 2.1.2 List -> List, Collection->Collection
- 2.1.3 Map -> Bean
- 2.1.4 Streams -> Collection
- 2.1.5 Enum -> Integer
- 2.2 更新Bean
- 2.3 类型转化
- 2.3.1 数字
- 2.3.2 时间
- 2.4 集成spring
- 2.5 复用mapper
- 三、复杂用法
- 3.1 组合
- 3.2 回调
- 3.3 表达式
- 3.4 选择填充
- 参考
前言
Java bean映射框架有很多,在之前我已经有一篇博文介绍了dozer, 它也是一个优秀的映射框架,但是作者已经不再维护了,不过作者在readme里推荐了另一个类似的框架 mapstruct ,所以准备开始学习它。
根据官网介绍,mapstruct 是一个Java注解处理器,用于为Java bean类生成类型安全和性能良好的映射类。使用 mapstruct ,只需要定义一个 Mapper 接口,声明需要映射的方法,在编译过程中,mapstruct 会自动生成该接口的实现类,实现将源对象映射到目标对象。
mapstruct 是在编译期动态生成getter/setter,而 dozer 是在运行期间使用反射,这是它俩最大的不同。dozer 最大的缺点就是性能不好,这与反射密切相关,而mapstruct是在编译时动态生成,这表示它的速度更快,而且 mapstruct 还支持不同名属性映射,这是 dozer比其它几个工具:Cglib 的 BeanCopier 、Apache 的 PropertyUtils 、Spring 的 BeanUtils 最大的优势。综上所述:看来使用 mapstruct 替代 dozer 更有未来。
mapstruct 是基于 JSR 269 实现的,JSR 269 是 JDK 引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。
一、pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aabond</groupId>
<artifactId>demomapstruct</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demomapstruct</name>
<description>demomapstruct</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<org.projectlombok.version>1.18.16</org.projectlombok.version>
<org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.aabond.demomapstruct.DemomapstructApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
二、简单使用
2.1 转换类型
2.1.1 Bean -> Bean
mapstruct 支持单个 bean 映射到另一个 bean ,默认同名映射,不同名使用 @Mapping 进行配置,不想进行映射的字段可以使用 ignore = true 来配置不参与映射。双向映射可以使用 @InheritInverseConfiguration 来简化配置
注意:使用 @Mapping 时,target 是要生成的对象属性,必填。而 source 是参数对象属性,选填
@Data
public class User {
private Long id;
private String username;
private String pawword;
}
@Data
public class UserDto {
private Long userId;
private String username;
private String pawword;
}
@Mapper
public interface UserMapper {
UserMapper INSTANCT = Mappers.getMapper(UserMapper.class);
@Mapping(target = "userId", source = "id")
@Mapping(target = "pawword", ignore = true)
UserDto userToUserDto(User user);
@InheritInverseConfiguration(name = "userToUserDto")
User userDtoToUser(UserDto userDto);
}
private static final Logger logger = LoggerFactory.getLogger(UserMapperTest.class);
@Test
void userToUserDtoTest() {
User user = new User();
user.setId(12L);
user.setUsername("张三");
user.setPawword("zedbw413bvw2-");
UserDto userDto = UserMapper.INSTANCT.userToUserDto(user);
logger.info("{}", userDto);
assertThat(userDto.getUserId(), is(user.getId()));
assertThat(userDto.getUsername(), is(user.getUsername()));
assertThat(userDto.getPassword(), is(nullValue()));
}
@Test
void userDtoToUserTest() {
UserDto userDto = new UserDto();
userDto.setUserId(1L);
userDto.setUsername("王二");
userDto.setPassword("zzzzzzz");
User user = UserMapper.INSTANCT.userDtoToUser(userDto);
logger.info("{}", user);
assertThat(user.getPawword(), is(nullValue()));
assertThat(user.getId(), is(userDto.getUserId()));
assertThat(user.getUsername(), is(userDto.getUsername()));
}
2.1.2 List -> List, Collection->Collection
在UserMapper增加两个方法,通过查看target下生成代码,可以看出实际还是调用userToUserDto,再组装成list或set
@Mapper
public interface UserMapper {
UserMapper INSTANCT = Mappers.getMapper(UserMapper.class);
@Mapping(target = "userId", source = "id")
UserDto userToUserDto(User user);
List<UserDto> userListToUserDtoList(List<User> list);
Set<UserDto> userListToUserDtoSet(List<User> list);
}
@Test
void userListToUserDtoListTest() {
User user = new User();
user.setId(12L);
user.setUsername("张三");
user.setPawword("zedbw413 bvw2-");
User user1 = new User();
user1.setId(19L);
user1.setUsername("李四");
user1.setPawword("vewrwwrw1231ev>/we");
UserDto userDto = UserMapper.INSTANCT.userToUserDto(user);
logger.info("{}", userDto);
List<User> userList = new ArrayList<>();
userList.add(user);
userList.add(user1);
userList.add(user);
List<UserDto> userDtos = UserMapper.INSTANCT.userListToUserDtoList(userList);
Set<UserDto> userDtoSet = UserMapper.INSTANCT.userListToUserDtoSet(userList);
logger.info("{}", userDtos);
logger.info("{}", userDtoSet);
assertThat(userDtos, is(hasSize(3)));
assertThat(userDtoSet, is(hasSize(2)));
}
2.1.3 Map -> Bean
mapstruct v1.5 支持了Map<String,?>
到bean
的转换,可以将一个 map 转为 bean ,简单的对象属性可以直接使用 Map<String, String> ,mapstruct 默认会将 String 转成Integer ,Long 等原生属性。但是如果是复杂转化,则需要自己定义转换规则。
@Mapper
public interface UserMapper {
UserMapper INSTANCT = Mappers.getMapper(UserMapper.class);
UserDto convertFromMap(Map<String, String> map);
}
@Test
void convertFromMap() {
Map<String, String> map = new HashMap<>();
map.put("userId", "8");
map.put("username", "王五");
UserDto userDto = UserMapper.INSTANCT.convertFromMap(map);
logger.info("{}", userDto);
assertThat(userDto.getUserId(), is(8L));
assertThat(userDto.getUsername(), is("王五"));
}
2.1.4 Streams -> Collection
List<UserDto> convertStream(Stream<User> user);
@Test
void convertStreamTest() {
User user = new User();
user.setId(12L);
user.setUsername("张三");
user.setPawword("zedbw413 bvw2-");
User user1 = new User();
user1.setId(19L);
user1.setUsername("李四");
user1.setPawword("vewrwwrw1231ev>/we");
List<UserDto> userDtos = UserMapper.INSTANCT.convertStream(Stream.of(user, user1));
assertThat(userDtos, is(hasSize(2)));
}
2.1.5 Enum -> Integer
支持枚举类型与Integer相互转化,需要提供对应的方法
public enum UserTypeEnum {
NORMAL(0), ADMIN(1), SUPER_ADMIN(2);
private Integer value;
UserTypeEnum(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
public static UserTypeEnum getEnumByValue(Integer value) {
return Arrays.stream(UserTypeEnum.values())
.filter(enumValue -> enumValue.getValue().equals(value))
.findFirst()
.orElse(null);
}
}
@Mapping(target = "userId", source = "id")
@Mapping(target = "userType", source = "userTypeEnum")
UserDto userToUserDto(User user);
@InheritInverseConfiguration(name = "userToUserDto")
User userDtoToUser(UserDto userDto);
default UserTypeEnum IntegerToUserTypeEnum(int value) {
return UserTypeEnum.getEnumByValue(value);
}
default Integer UserTypeEnumToInteger(UserTypeEnum userTypeEnum) {
return userTypeEnum.getValue();
}
@Data
public class User {
private Long id;
private String username;
private String pawword;
private UserTypeEnum userTypeEnum;
}
@Data
public class UserDto {
private Long userId;
private String username;
private Integer userType;
}
@Test
void userToUserDtoEnumTest() {
User user = new User();
user.setId(12L);
user.setUsername("张三");
user.setPawword("zedbw413 bvw2-");
user.setUserTypeEnum(UserTypeEnum.ADMIN);
UserDto userDto = UserMapper.INSTANCT.userToUserDto(user);
logger.info("{}", userDto);
assertThat(userDto.getUserType(), is(UserTypeEnum.ADMIN.getValue()));
}
2.2 更新Bean
有时候是想更新bean,而不是生成bean, 下面以根据 user 更新 userDto 为例
@InheritConfiguration 可以继承配置
@InheritConfiguration
void updateUserDto(User user, @MappingTarget UserDto userDto);
@Test
void updateUserDtoTest() {
User user = new User();
user.setId(12L);
user.setUsername("张三");
user.setPawword("zedbw413 bvw2-");
UserDto userDto = new UserDto();
userDto.setUserId(100L);
UserMapper.INSTANCT.updateUserDto(user, userDto);
logger.info("{}", userDto);
assertThat(userDto.getUserId(), is(12L));
}
2.3 类型转化
@Data
public class Car {
private String make;
private int numberOfSeats;
private String price;
private LocalDate sellDate;
}
@Data
public class CarDto {
private String make;
private int seatCount;
private String type;
private Double price;
private String sellDate;
}
2.3.1 数字
数字类型、字符串相互转换,字符串与数字之间通过java.text.DecimalFormat
转化
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(target = "price", numberFormat = "#.00")
CarDto carToCarDto(Car car);
@IterableMapping(numberFormat = "#.00")
List<String> prices(List<Integer> prices);
}
@Test
void carToCarDtoTest() {
Car car = new Car();
car.setPrice("123400000");
car.setMake("五菱");
car.setNumberOfSeats(6);
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
logger.info("{}", carDto);
assertThat(carDto.getPrice(), is(123400000D));
}
@Test
void pricesTest() {
List<Integer> prices = new ArrayList<>();
prices.add(12);
prices.add(34);
prices.add(340);
List<String> list = CarMapper.INSTANCE.prices(prices);
logger.info("{}", list);
assertThat(list, is(Arrays.asList("12.00", "34.00", "340.00")));
}
2.3.2 时间
@Mapping(target = "sellDate", dateFormat = "dd.MM.yyyy")
CarDto carToCarDto(Car car);
@IterableMapping(dateFormat = "yyyy-MM-dd")
List<String> stringListToDateList(List<LocalDate> dates);
@Test
void carToCarDtoTest() {
Car car = new Car();
car.setPrice("123400000");
car.setMake("五菱");
car.setNumberOfSeats(6);
car.setSellDate(LocalDate.of(2022, 1, 1));
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
logger.info("{}", carDto);
assertThat(carDto.getSellDate(), is("01.01.2022"));
}
@Test
void stringListToDateListTest() {
List<LocalDate> dates = Arrays.asList(LocalDate.of(1999, 1, 1), LocalDate.of(2022, 12, 12));
List<String> strings = CarMapper.INSTANCE.stringListToDateList(dates);
logger.info("{}", strings);
assertThat(strings, is(Arrays.asList("1999-01-01", "2022-12-12")));
}
2.4 集成spring
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface CarMapper {
@IterableMapping(numberFormat = "#.00")
List<String> prices(List<Integer> prices);
}
@Autowired
private CarMapper carMapper;
@Test
void pricesSpringTest() {
List<Integer> prices = new ArrayList<>();
prices.add(12);
prices.add(34);
prices.add(340);
List<String> list = carMapper.prices(prices);
logger.info("{}", list);
assertThat(list, is(Arrays.asList("12.00", "34.00", "340.00")));
}
2.5 复用mapper
mapstruct提供复用功能,可以将其他mapper或手写转化方法的类,使用uses可被调用
@Data
public class User {
private Long id;
private String username;
private String pawword;
private LocalDate birthdate;
}
@Data
public class UserDto {
private Long userId;
private String username;
private String birthdate;
}
@Mapper(uses = {DateMapper.class})
public interface UserMapper{
UserMapper INSTANCT = Mappers.getMapper(UserMapper.class);
@Mapping(target = "userId", source = "id")
UserDto userToUserDto(User user);
}
public class DateMapper {
private static final String DATE_PATTERN = "yyyy-MM-dd";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
public String asString(LocalDate date) {
return date != null ? date.format(DATE_FORMATTER): null;
}
public LocalDate asDate(String date) {
return LocalDate.parse(date, DATE_FORMATTER);
}
}
void userToUserVo() {
User user = new User();
user.setId(12L);
user.setUsername("张三");
user.setPawword("zedbw413 bvw2-");
user.setBirthdate(LocalDate.of(1999, 1, 1));
UserDto userDto = UserMapper.INSTANCT.userToUserDto(user);
logger.info("{}", userDto);
assertThat(userDto.getBirthdate(), is("1999-01-01"));
}
三、复杂用法
3.1 组合
将多个Bean合并到一个Dto中
@Data
public class UserInfoDto {
private Integer userId;
private String userName;
private String make;
private Integer num;
}
@Mapping(target = "userId", source = "user.id")
@Mapping(target = "userName", source = "user.username")
@Mapping(target = "make", source = "car.make")
UserInfoDto toUserInfoDto(User user, Car car, String num);
@Test
void toUserInfoDtoTest() {
User user = new User();
user.setId(12L);
user.setUsername("张三");
user.setPawword("zedbw413 bvw2-");
user.setUserTypeEnum(UserTypeEnum.ADMIN);
Car car = new Car();
car.setPrice("123400000");
car.setMake("五菱");
car.setNumberOfSeats(6);
car.setSellDate(LocalDate.of(2022, 1, 1));
UserInfoDto userInfoDto = UserMapper.INSTANCT.toUserInfoDto(user, car, "2");
logger.info("{}", userInfoDto);
assertThat(userInfoDto.getUserId(), is(user.getId().intValue()));
assertThat(userInfoDto.getUserName(), is(user.getUsername()));
assertThat(userInfoDto.getMake(), is(car.getMake()));
assertThat(userInfoDto.getNum(), is(2));
}
3.2 回调
mapstruct 提供两个注解 @BeforeMapping 和 @AfterMapping ,用于回调,
@BeforeMapping
default void before(Car car) {
car.setMake(car.getMake() + " + before");
}
@AfterMapping
default void after(@MappingTarget CarDto carDto) {
carDto.setMake(carDto.getMake() + " + after");
}
@Test
void beforeAfterTest() {
Car car = new Car();
car.setPrice("123400000");
car.setMake("五菱");
car.setNumberOfSeats(6);
car.setSellDate(LocalDate.of(2022, 1, 1));
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
logger.info("{}", carDto);
assertThat(carDto.getMake(), is("五菱 + before + after"));
}
3.3 表达式
mapstruct 支持通过表达式表示对属性进行填充,暂时只支持java
@Data
public class CarDto {
private String make;
private int seatCount;
private String type;
private Double price;
private String sellDate;
private List<String> list;
}
import java.util.Arrays;
@Mapper(imports = Arrays.class)
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(target = "list", expression = "java(Arrays.asList(name, car.getMake(), car.getPrice()))")
CarDto experession(Car car, String name);
}
@Test
void experessionTest() {
Car car = new Car();
car.setPrice("123400000");
car.setMake("五菱");
car.setNumberOfSeats(6);
car.setSellDate(LocalDate.of(2022, 1, 1));
CarDto test = CarMapper.INSTANCE.experession(car, "test");
logger.info("{}", test);
List<String> list = new ArrayList<>();
list.add("test");
list.add(car.getMake());
list.add(car.getPrice());
assertThat(test.getList(), is(list));
}
3.4 选择填充
mapstruct 支持 用户编写自定义条件方法,返回值需为boolean, 这些方法将被调用以检查是否需要映射属性
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(target = "price", numberFormat = "#.00")
@Mapping(target = "sellDate", dateFormat = "yyyy.MM.dd")
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToCarDto(Car car);
@Condition
default boolean isNotEmpty(String value) {
return value != null && !value.isEmpty();
}
}
@Test
void testCondition() {
Car car = new Car();
car.setPrice("");
car.setMake("");
car.setNumberOfSeats(6);
car.setSellDate(LocalDate.of(2022, 1, 1));
CarDto carDto = carMapper.carToCarDto(car);
assertThat(carDto.getMake(), is(nullValue()));
assertThat(carDto.getPrice(), is(nullValue()));
}