别再写满屏的 get & set 了,太 Low!试试 MapStruct 高级玩法!
点击关注公众号,Java干货及时送达
接上篇,如果你还不知道 MapStruct 是什么的,建议你看下栈长之前分享的《干掉 BeanUtils!试试这款 Bean 自动映射工具,真心强大!!》你就清楚了。
上篇介绍了 MapStruct 的基本概念,以及单个对象、对象列表的映射实践,栈长看了上篇有一些留言,当然,萝卜白菜各有所爱,喜欢就用,不喜欢就不用,没必要争执,工具好不好,不一定适合所有人,大家开心就好。
这篇来几个高级点的映射玩法,别再写满屏的 get-set 了,太 Low!MapStruct 高级玩法,这篇栈长带你上正道!
1、自定义映射
当我们映射 DTO 的时候,如果某些参数的值 MapStruct 的映射配置不能满足要求,可以使用自定义方法。
新增两个 DTO 类:
UserCustomDTO 类里面包含了 UserExtDTO 对象。
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@Data
public class UserCustomDTO {
private String name;
private int sex;
private boolean married;
private String birthday;
private String regDate;
private UserExtDTO userExtDTO;
private String memo;
}
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@Data
public class UserExtDTO {
private String regSource;
private String favorite;
private String school;
private int kids;
private String memo;
}
自定义映射:
如果 UserExtDTO 对象不想使用默认的映射,可以添加一个该参数的自定义映射方法。
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@Mapper(componentModel = "spring")
public interface UserCustomStruct {
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(target = "regDate", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(userDO.getRegDate(),\"yyyy-MM-dd HH:mm:ss\"))")
@Mapping(source = "userExtDO", target = "userExtDTO")
@Mapping(target = "memo", ignore = true)
UserCustomDTO toUserCustomDTO(UserDO userDO);
default UserExtDTO toUserExtDTO(UserExtDO userExtDO) {
UserExtDTO userExtDTO = new UserExtDTO();
userExtDTO.setKids(userExtDO.getKids());
userExtDTO.setFavorite(userExtDO.getFavorite());
// 覆盖这两个值
userExtDTO.setRegSource("默认来源");
userExtDTO.setSchool("默认学校");
return userExtDTO;
}
}
当映射 UserExtDTO 对象的时候,会自动调用该接口中的自定义 toUserExtDTO 方法,完成自定义映射。
来看下生成的实现类源码:
@Component
public class UserCustomStructImpl implements UserCustomStruct {
public UserCustomStructImpl() {
}
public UserCustomDTO toUserCustomDTO(UserDO userDO) {
if (userDO == null) {
return null;
} else {
UserCustomDTO userCustomDTO = new UserCustomDTO();
if (userDO.getBirthday() != null) {
userCustomDTO.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(userDO.getBirthday()));
}
userCustomDTO.setUserExtDTO(this.toUserExtDTO(userDO.getUserExtDO()));
userCustomDTO.setName(userDO.getName());
userCustomDTO.setSex(userDO.getSex());
userCustomDTO.setMarried(userDO.isMarried());
userCustomDTO.setRegDate(DateFormatUtils.format(userDO.getRegDate(), "yyyy-MM-dd HH:mm:ss"));
return userCustomDTO;
}
}
}
没错,setUserExtDTO 方法调用了 this.toUserExtDTO 自定义方法映射。
Spring Boot 基础这篇就不介绍了,系列基础教程和示例源码可以看这里:https://github.com/javastacks/spring-boot-best-practice
测试一下:
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserCustomStructTest {
@Autowired
private UserCustomStruct userCustomStruct;
@Test
public void test1() {
UserExtDO userExtDO = new UserExtDO();
userExtDO.setRegSource("公众号:Java技术栈");
userExtDO.setFavorite("写代码");
userExtDO.setSchool("社会大学");
userExtDO.setKids(1);
UserDO userDO = new UserDO();
userDO.setName("栈长自定义方法");
userDO.setSex(1);
userDO.setAge(18);
userDO.setBirthday(new Date());
userDO.setPhone("18888888888");
userDO.setMarried(true);
userDO.setRegDate(new Date());
userDO.setMemo("666");
userDO.setUserExtDO(userExtDO);
UserCustomDTO userCustomDTO = userCustomStruct.toUserCustomDTO(userDO);
System.out.println("=====自定义方法=====");
System.out.println(userCustomDTO);
}
}
输出结果:
可以看到自定义方法覆盖的两个值,结果验证成功。
2、多参数映射
之前介绍的映射方法中只有一个参数,如果有多个参数映射成一个 DTO,该怎么弄呢?
比如:有两具单独的 DO,UserDO、UserAddressDO 映射成 UserMultiDTO。
直接上代码:
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@Mapper(componentModel = "spring")
public interface UserMultiStruct {
@Mapping(source = "userDO.birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(target = "userDO.regDate", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(user.getRegDate(),\"yyyy-MM-dd HH:mm:ss\"))")
@Mapping(source = "userAddressDO.postcode", target = "postcode")
@Mapping(source = "userAddressDO.address", target = "address")
@Mapping(target = "memo", ignore = true)
UserMultiDTO toUserMultiDTO(UserDO userDO, UserAddressDO userAddressDO);
}
直接使用指定的 对象名.属性名
形式映射即可。
来看下生成的实现类源码:
@Component
public class UserMultiStructImpl implements UserMultiStruct {
public UserMultiStructImpl() {
}
public UserMultiDTO toUserMultiDTO(UserDO userDO, UserAddressDO userAddressDO) {
if (userDO == null && userAddressDO == null) {
return null;
} else {
UserMultiDTO userMultiDTO = new UserMultiDTO();
if (userDO != null) {
if (userDO.getBirthday() != null) {
userMultiDTO.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(userDO.getBirthday()));
}
userMultiDTO.setName(userDO.getName());
userMultiDTO.setSex(userDO.getSex());
userMultiDTO.setMarried(userDO.isMarried());
if (userDO.getRegDate() != null) {
userMultiDTO.setRegDate((new SimpleDateFormat()).format(userDO.getRegDate()));
}
}
if (userAddressDO != null) {
userMultiDTO.setPostcode(userAddressDO.getPostcode());
userMultiDTO.setAddress(userAddressDO.getAddress());
}
return userMultiDTO;
}
}
}
测试一下:
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMultiStructTest {
@Autowired
private UserMultiStruct userMultiStruct;
@Test
public void test1() {
UserDO userDO = new UserDO();
userDO.setName("多参数映射");
userDO.setSex(1);
userDO.setAge(18);
userDO.setBirthday(new Date());
userDO.setPhone("18888888888");
userDO.setMarried(true);
userDO.setRegDate(new Date());
userDO.setMemo("666");
UserAddressDO userAddressDO = new UserAddressDO();
userAddressDO.setProvince("广东省");
userAddressDO.setCity("深圳市");
userAddressDO.setPostcode("666666");
userAddressDO.setAddress("001号大街Java技术栈公众号");
userAddressDO.setMemo("地址信息");
UserMultiDTO userMultiDTO = userMultiStruct.toUserMultiDTO(userDO, userAddressDO);
System.out.println("=====多参数映射=====");
System.out.println(userMultiDTO);
}
}
输出结果:
个人信息和地址信息都输出来了,结果验证成功。
本文实战源代码完整版已经上传:
https://github.com/javastacks/spring-boot-best-practice
3、嵌套映射
如果一个 DTO 中的值都是从一个对象中的多个嵌套对象映射时,如果不想一个个写映射,目标可以用 . 表示。
直接上代码:
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@Mapper(componentModel = "spring")
public interface UserNestedStruct {
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(target = "regDate", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(userNestedDO.getRegDate(),\"yyyy-MM-dd HH:mm:ss\"))")
@Mapping(source = "userAddressDO", target = ".")
@Mapping(source = "userExtDO", target = ".")
@Mapping(source = "userExtDO.memo", target = "memo")
UserNestedDTO toUserNestedDTO(UserNestedDO userNestedDO);
}
如果嵌套对象中出现重名的映射冲突,可以手动指定来源哪个嵌套对象。
来看下生成的实现类源码:
@Component
public class UserNestedStructImpl implements UserNestedStruct {
public UserNestedStructImpl() {
}
public UserNestedDTO toUserNestedDTO(UserNestedDO userNestedDO) {
if (userNestedDO == null) {
return null;
} else {
UserNestedDTO userNestedDTO = new UserNestedDTO();
if (userNestedDO.getBirthday() != null) {
userNestedDTO.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(userNestedDO.getBirthday()));
}
userNestedDTO.setMemo(this.userNestedDOUserExtDOMemo(userNestedDO));
userNestedDTO.setCity(this.userNestedDOUserAddressDOCity(userNestedDO));
userNestedDTO.setAddress(this.userNestedDOUserAddressDOAddress(userNestedDO));
userNestedDTO.setRegSource(this.userNestedDOUserExtDORegSource(userNestedDO));
userNestedDTO.setFavorite(this.userNestedDOUserExtDOFavorite(userNestedDO));
userNestedDTO.setSchool(this.userNestedDOUserExtDOSchool(userNestedDO));
userNestedDTO.setName(userNestedDO.getName());
userNestedDTO.setSex(userNestedDO.getSex());
userNestedDTO.setMarried(userNestedDO.isMarried());
userNestedDTO.setRegDate(DateFormatUtils.format(userNestedDO.getRegDate(), "yyyy-MM-dd HH:mm:ss"));
return userNestedDTO;
}
}
private String userNestedDOUserExtDOMemo(UserNestedDO userNestedDO) {
if (userNestedDO == null) {
return null;
} else {
UserExtDO userExtDO = userNestedDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String memo = userExtDO.getMemo();
return memo == null ? null : memo;
}
}
}
private String userNestedDOUserAddressDOCity(UserNestedDO userNestedDO) {
if (userNestedDO == null) {
return null;
} else {
UserAddressDO userAddressDO = userNestedDO.getUserAddressDO();
if (userAddressDO == null) {
return null;
} else {
String city = userAddressDO.getCity();
return city == null ? null : city;
}
}
}
private String userNestedDOUserAddressDOAddress(UserNestedDO userNestedDO) {
if (userNestedDO == null) {
return null;
} else {
UserAddressDO userAddressDO = userNestedDO.getUserAddressDO();
if (userAddressDO == null) {
return null;
} else {
String address = userAddressDO.getAddress();
return address == null ? null : address;
}
}
}
private String userNestedDOUserExtDORegSource(UserNestedDO userNestedDO) {
if (userNestedDO == null) {
return null;
} else {
UserExtDO userExtDO = userNestedDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String regSource = userExtDO.getRegSource();
return regSource == null ? null : regSource;
}
}
}
private String userNestedDOUserExtDOFavorite(UserNestedDO userNestedDO) {
if (userNestedDO == null) {
return null;
} else {
UserExtDO userExtDO = userNestedDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String favorite = userExtDO.getFavorite();
return favorite == null ? null : favorite;
}
}
}
private String userNestedDOUserExtDOSchool(UserNestedDO userNestedDO) {
if (userNestedDO == null) {
return null;
} else {
UserExtDO userExtDO = userNestedDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String school = userExtDO.getSchool();
return school == null ? null : school;
}
}
}
}
从源码可以看到,从嵌套对象来的值都会新增一个方法判断一下,以避免出现空指定。
测试一下:
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserNestedStructTest {
@Autowired
private UserNestedStruct userNestedStruct;
@Test
public void test1() {
UserExtDO userExtDO = new UserExtDO();
userExtDO.setRegSource("公众号:Java技术栈");
userExtDO.setFavorite("写代码");
userExtDO.setSchool("社会大学");
userExtDO.setKids(1);
userExtDO.setMemo("扩展信息");
UserAddressDO userAddressDO = new UserAddressDO();
userAddressDO.setProvince("广东省");
userAddressDO.setCity("深圳市");
userAddressDO.setPostcode("666666");
userAddressDO.setAddress("001号大街Java技术栈公众号");
userAddressDO.setMemo("地址信息");
UserNestedDO userNestedDO = new UserNestedDO();
userNestedDO.setName("栈长嵌套映射");
userNestedDO.setSex(1);
userNestedDO.setAge(18);
userNestedDO.setBirthday(new Date());
userNestedDO.setPhone("18888888888");
userNestedDO.setMarried(true);
userNestedDO.setRegDate(new Date());
userNestedDO.setMemo("666");
userNestedDO.setUserExtDO(userExtDO);
userNestedDO.setUserAddressDO(userAddressDO);
UserNestedDTO userNestedDTO = userNestedStruct.toUserNestedDTO(userNestedDO);
System.out.println("=====嵌套映射=====");
System.out.println(userNestedDTO);
}
}
输出结果:
可以看到嵌套对象值,并且 memo 也是从指定的嵌套对象来的,结果验证成功。
4、映射现有实例
以上介绍的都是映射并生成一个新的 DTO 实例,如果是已有的现有 DTO 实例呢,该怎么映射呢?
直接上代码:
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@Mapper(componentModel = "spring")
public interface UserExistStruct {
@Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
@Mapping(target = "regDate", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(userDO.getRegDate(),\"yyyy-MM-dd HH:mm:ss\"))")
@Mapping(source = "userExtDO.regSource", target = "registerSource")
@Mapping(source = "userExtDO.favorite", target = "favorite")
@Mapping(target = "name", ignore = true)
@Mapping(target = "memo", ignore = true)
void toUserShowDTO(@MappingTarget UserShowDTO userShowDTO, UserDO userDO);
}
在方法上新增 DTO 对象参数并使用 @MappingTarget
对象修饰,参数位置可以调换。
来看下生成的实现类源码:
@Component
public class UserExistStructImpl implements UserExistStruct {
public UserExistStructImpl() {
}
public void toUserShowDTO(UserShowDTO userShowDTO, UserDO userDO) {
if (userDO != null) {
if (userDO.getBirthday() != null) {
userShowDTO.setBirthday((new SimpleDateFormat("yyyy-MM-dd")).format(userDO.getBirthday()));
} else {
userShowDTO.setBirthday((String)null);
}
userShowDTO.setRegisterSource(this.userDOUserExtDORegSource(userDO));
userShowDTO.setFavorite(this.userDOUserExtDOFavorite(userDO));
userShowDTO.setSex(userDO.getSex());
userShowDTO.setMarried(userDO.isMarried());
userShowDTO.setRegDate(DateFormatUtils.format(userDO.getRegDate(), "yyyy-MM-dd HH:mm:ss"));
}
}
private String userDOUserExtDORegSource(UserDO userDO) {
if (userDO == null) {
return null;
} else {
UserExtDO userExtDO = userDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String regSource = userExtDO.getRegSource();
return regSource == null ? null : regSource;
}
}
}
private String userDOUserExtDOFavorite(UserDO userDO) {
if (userDO == null) {
return null;
} else {
UserExtDO userExtDO = userDO.getUserExtDO();
if (userExtDO == null) {
return null;
} else {
String favorite = userExtDO.getFavorite();
return favorite == null ? null : favorite;
}
}
}
}
userShowDTO 是作为方法参数传入的,而不是新创建的。
测试一下:
/**
* 微信公众号:Java技术栈
* @author 栈长
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserExistStructTest {
@Autowired
private UserExistStruct userExistStruct;
@Test
public void test1() {
UserExtDO userExtDO = new UserExtDO();
userExtDO.setRegSource("公众号:Java技术栈");
userExtDO.setFavorite("写代码");
userExtDO.setSchool("社会大学");
UserDO userDO = new UserDO();
userDO.setName("栈长");
userDO.setSex(1);
userDO.setAge(18);
userDO.setBirthday(new Date());
userDO.setPhone("18888888888");
userDO.setMarried(true);
userDO.setRegDate(new Date());
userDO.setMemo("666");
userDO.setUserExtDO(userExtDO);
System.out.println("=====映射现有实例前=====");
UserShowDTO userShowDTO = new UserShowDTO();
userShowDTO.setName("栈长NAME");
userShowDTO.setMemo("栈长MEMO");
System.out.println(userShowDTO);
System.out.println("=====映射现有实例后=====");
userExistStruct.toUserShowDTO(userShowDTO, userDO);
System.out.println(userShowDTO);
}
}
输出结果:
可以看到已有 DTO 对象的值及新映射的值,结果验证成功。
注意:默认是以覆盖原有值的方式映射的,如果要保留原有 XX 的值,使用 ignore 忽略即可
总结
本文栈长介绍了 MapStruct 的 4 个高级玩法,足以应对各种 Bean 类映射了,其实还有很多复杂的、个性化用法,一篇难以写完,栈长后面有时间会整理出来,陆续给大家分享。
感兴趣的也可以参考官方文档:
https://mapstruct.org/documentation/reference-guide/
另外,栈长一直介绍的是 DO --> DTO 的映射,其实反过来 DTO --> DO、BO 也是一样的,只是对象名称不一样,映射的用法是一样的,这样在服务 A 接收到服务 B 过来的 DTO 数据时,可以再进行一次反射映射供业务使用。
本文实战源代码完整版已经上传:
https://github.com/javastacks/spring-boot-best-practice
欢迎 Star 学习,后面 Spring Boot 示例都会在这上面提供!
好了,今天的分享就到这了,后面我还会陆续解读更多的好玩的 Java 技术,关注公众号Java技术栈第一时间推送。另外,我也将 Spring Boot 系列主流面试题和参考答案都整理好了,关注公众号Java技术栈回复关键字 "面试" 进行刷题。
最后,觉得我的文章对你用收获的话,动动小手,给个在看、转发,原创不易,栈长需要你的鼓励。
版权申明:本文系公众号 "Java技术栈" 原创,原创实属不易,转载、引用本文内容请注明出处,禁止抄袭、洗稿,请自重,尊重技术人劳动成果和知识产权,抄袭者一律举报+投诉,并保留追究其法律责任的权利。
关注Java技术栈看更多干货