1、什么是MapStruct
1.1 JavaBean 的困扰
对于代码中 JavaBean之间的转换, 一直是困扰我很久的事情。在开发的时候我看到业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感,却又不得不存在。我后来想的一个办法就是通过反射,或者自己写很多的转换器。
第一种通过反射的方法确实比较方便,但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。虽然我们可以进行反射信息的缓存来提高性能。但是像这种的话,需要类型和名称都一样才会进行映射,有很多时候,由于不同的团队之间使用的名词不一样,还是需要很多的手动 set/get 等功能。
第二种的话就是会很浪费时间,而且在添加新的字段的时候也要进行方法的修改。不过,由于不需要进行反射,其性能是很高的。
1.2 MapStruct 带来的改变
MapSturct 是一个生成类型安全,高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。
- 注解处理器
- 可以生成 JavaBean 之间那的映射代码
- 类型安全,高性能,无依赖性
2、MapStruct 入门
2.1 添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.20</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
|
2.2 po类
1 2 3 4 5 6 7
| @Data public class User { private Integer id; private String name; private String address; private Date birth; }
|
2.3 dto类
1 2 3 4 5 6 7
| @Data public class UserDto implements Serializable { private Integer id; private String name; private String address; private Date birth; }
|
2.4 创建转换接口
1 2 3 4 5 6 7 8
| @Mapper public interface UserMapper { UserDto userToUserDto(User user); List<UserDto> userToUserDto(List<User> users); }
|
2.5 测试方法
1 2 3 4 5 6 7 8 9 10 11
| @Test public void userPoToUserDto() { User user =new User(); user.setId(1); user.setName("myx"); user.setAddress("河北沧州"); user.setBirth(new Date()); UserMapper mapper = Mappers.getMapper(UserMapper.class); UserDto userDto = mapper.userToUserDto(user); System.out.println(userDto); }
|
2.6 运行效果
2.7 查看编译的class
底层通过自动取值赋值操作完成
3、MapStruct优点分析
3.1 性能高
这是相对反射来说的,反射需要去读取字节码的内容,花销会比较大。而通过 MapStruct 来生成的代码,其类似于人手写。速度上可以得到保证。
3.2 使用简单
如果是完全映射的,使用起来肯定没有反射简单。用类似 BeanUtils 这些工具一条语句就搞定了。但是,如果需要进行特殊的匹配(特殊类型转换,多对一转换等),其相对来说也是比较简单的。
基本上,使用的时候,我们只需要声明一个接口,接口下写对应的方法,就可以使用了。当然,如果有特殊情况,是需要额外处理的。
3.3 代码独立
生成的代码是对立的,没有运行时的依赖。
3.4 易于 debug
在我们生成的代码中,我们可以轻易的进行 debug。
4、MapStruct使用案例
4.1 属性名称相同
在实现类的时候,如果属性名称相同,则会进行对应的转化。通过此种方式,我们可以快速的编写出转换的方法。(入门案例)
4.2 属性名不相同
属性名不相同,在需要进行互相转化的时候,则我们可以通过@Mapping
注解来进行转化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Data public class UserDto implements Serializable { private Integer id; private String name; private String address; private Date birth; private String password; } @Data public class User { private Integer id; private String name; private String address; private Date birth; private String pwd; } @Mapper public interface UserMapper { @Mappings({ @Mapping(source = "pwd",target = "password") }) UserDto userToUserDto(User user); }
|
- source 需要转换的对接,通常是入参
- target 转换的对接,通常是出参
- ignore 忽略,默认false不忽略,需要忽略设置为true
- defaultValue 默认值
- expressions 可以通过表达式来构造一些简单的转化关系。虽然设计的时候想兼容很多语言,不过目前只能写Java代码。
1 2 3 4 5
| @Mappings({ @Mapping(source = "birthdate", target = "birth"),//属性名不一致映射 @Mapping(target = "birthformat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthdate(),\"yyyy-MM-dd HH:mm:ss\"))"),//自定义属性通过java代码映射 }) public PersonVo PersonToPersonVo(Person person);
|
这里用到演示了如何使用TimeAndFormat对time和format操作,这里必须要指定需要使用的Java类的完整包名,不然编译的时候不知道你使用哪个Java类,会报错。
1 2 3 4 5 6 7 8 9 10 11 12
| @Test public void userPoToUserDto() { User user =new User(); user.setId(1); user.setName("myx"); user.setAddress("河北沧州"); user.setBirth(new Date()); user.setPwd("123456"); UserMapper mapper = Mappers.getMapper(UserMapper.class); UserDto userDto = mapper.userToUserDto(user); System.out.println(userDto); }
|
4.3 转换非基础类型属性
如果subUser与subUserDto字段名称相同直接配置即可完成(对象类型,包括list)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Data public class UserDto implements Serializable { private Integer id; private String name; private String address; private Date birth; private String password; private List<SubUserDto> subUserDto; } @Data public class User { private Integer id; private String name; private String address; private Date birth; private String pwd; private List<SubUser> subUser; } @Mappings({ @Mapping(source = "pwd",target = "password"), @Mapping(source = "subUser", target = "subUserDto") }) UserDto userToUserDto(User user);
|
4.4 Mapper 中使用自定义的转换
有时候,对于某些类型,无法通过代码生成器的形式来进行处理。那么, 就需要自定义的方法来进行转换。这时候,我们可以在接口(同一个接口,后续还有调用别的 Mapper 的方法)中定义默认方法(Java8及之后)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Data public class UserDto implements Serializable { private Integer id; private String name; private String address; private Date birth; private String password; private SubUserDto subUserDto; }
@Data public class SubUserDto { private Boolean result; private String name; } @Data public class User { private Integer id; private String name; private String address; private Date birth; private String pwd; private SubUser subUser; }
@Data public class SubUser { private Integer deleted; private String name; } @Mapper public interface UserMapper { @Mappings({ @Mapping(source = "pwd",target = "password"), @Mapping(source = "subUser", target = "subUserDto") }) UserDto userToUserDto(User user);
default SubUserDto subSource2subTarget(SubUser subUser) { if (subUser == null) { return null; } SubUserDto subUserDto = new SubUserDto(); subUserDto.setResult(!subUser.getDeleted().equals(0)); subUserDto.setName(subUser.getName()==null?"":subUser.getName()); return subUserDto; } }
|
只能存在一个default修饰的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Test public void userPoToUserDto() { User user =new User(); user.setId(1); user.setName("myx"); user.setAddress("河北沧州"); user.setBirth(new Date()); user.setPwd("123456"); SubUser subUser =new SubUser(); subUser.setDeleted(0); subUser.setName("rkw"); user.setSubUser(subUser); UserMapper mapper = Mappers.getMapper(UserMapper.class); UserDto userDto = mapper.userToUserDto(user); System.out.println(userDto); }
|
4.5 多转一
我们在实际的业务中少不了将多个对象转换成一个的场景。MapStruct 当然也支持多转一的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| ![5](http: public class SubUser { private Integer deleted; private String name; } @Data public class User { private Integer id; private String name; private String address; private Date birth; private String pwd; } @Mapper public interface UserMapper { @Mappings({ @Mapping(source = "user.pwd",target = "password"), @Mapping(source = "subUser.name", target = "name") }) NewUserDto userToUserDto(User user,SubUser subUser); } @Test public void userPoToUserDto() { User user =new User(); user.setId(1); user.setName("myx"); user.setAddress("河北沧州"); user.setBirth(new Date()); user.setPwd("123456"); SubUser subUser =new SubUser(); subUser.setDeleted(0); subUser.setName("rkw"); UserMapper mapper = Mappers.getMapper(UserMapper.class); NewUserDto userDto = mapper.userToUserDto(user,subUser); System.out.println(userDto); }
|
4.5.1 遵循原则
- 当多个对象中, 有其中一个为 null, 则会直接返回 null
- 如一对一转换一样, 属性通过名字来自动匹配。因此, 名称和类型相同的不需要进行特殊处理
- 当多个原对象中,有相同名字的属性时,需要通过 @Mapping 注解来具体的指定, 以免出现歧义(不指定会报错)。如上面的 name
属性也可以直接从传入的参数来赋值
1 2 3
| @Mapping(source = "person.description", target = "description") @Mapping(source = "name", target = "name") DeliveryAddress personAndAddressToDeliveryAddressDto(Person person, String name);
|
4.6 更新 Bean 对象
有时候,我们不是想返回一个新的 Bean 对象,而是希望更新传入对象的一些属性。这个在实际的时候也会经常使用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @Mapper public interface UserMapper {
NewUserDto userToNewUserDto(User user);
void updateDeliveryAddressFromAddress(SubUser subUser,@MappingTarget NewUserDto newUserDto); } @Test public void userPoToUserDto() { User user =new User(); user.setId(1); user.setName("myx"); user.setAddress("河北沧州"); user.setBirth(new Date()); SubUser subUser =new SubUser(); subUser.setDeleted(0); subUser.setName("rkw"); UserMapper mapper = Mappers.getMapper(UserMapper.class); NewUserDto userDto = mapper.userToNewUserDto(user); mapper.updateDeliveryAddressFromAddress(subUser,userDto); System.out.println(userDto); }
|
4.7 map映射
1 2 3 4 5 6 7 8 9
| @MapMapping(valueDateFormat ="yyyy-MM-dd HH:mm:ss") public Map<String ,String> DateMapToStringMap(Map<String,Date> sourceMap); @Test public void mapMappingTest(){ Map<String,Date> map=new HashMap<>(); map.put("key1",new Date()); map.put("key2",new Date(new Date().getTime()+9800000)); Map<String, String> stringObjectMap = TestMapper.MAPPER.DateMapToStringMap(map); }
|
4.8 多级嵌套
只需要在mapper接口中定义相关的类型转换方法即可,list类型也适用
4.8.1 方式1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Data public class User { private Integer id; private String name; private String address; private Date birth; private Boolean isDisable; private List<SubUser> user; }
@Data public class SubUser { private Integer deleted; private String name; private List<SubSubUser> subUser; } @Data public class SubSubUser { private String aaa; private String ccc; } @Data public class UserDto implements Serializable { private Integer id; private String name; private String address; private Date birth; private String isDisable; private List<SubUserDto> user; } @Data public class SubUserDto { private Integer deleted; private String name; private List<SubSubUserDto> subUser; } @Data public class SubSubUserDto { private String aaa; private String bbb; } @Mapper public interface UserMapper {
UserDto userToNewUserDto(User user); @Mapping(source = "ccc",target = "bbb") SubSubUserDto bbb(SubSubUser subSubUser);
}
|
4.8.2 方式2
通过uses配置类型转换
1 2 3 4 5 6 7 8 9
| @Mapper(uses = {TestMapper.class}) public interface UserMapper { UserDto userToNewUserDto(User user); } @Mapper public interface TestMapper { @Mapping(source = "ccc",target = "bbb") SubSubUserDto bbb(SubSubUser subSubUser); }
|
5、获取 mapper
5.1 通过 Mapper 工厂获取
我们都是通过 Mappers.getMapper(xxx.class)
的方式来进行对应 Mapper 的获取。此种方法为通过 Mapper 工厂获取。
如果是此种方法,约定俗成的是在接口内定义一个接口本身的实例 INSTANCE, 以方便获取对应的实例。
1 2 3 4 5 6 7
| @Mapper public interface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
}
|
这样在调用的时候,我们就不需要在重复的去实例化对象了。类似下面
1
| Target target = SourceMapper.INSTANCE.source2target(source);
|
5.2 使用依赖注入
对于 Web 开发,依赖注入应该很熟悉。MapSturct 也支持使用依赖注入,同时也推荐使用依赖注入。
1
| @Mapper(componentModel = "spring")
|
5.3 依赖注入策略
可以选择是通过构造方法或者属性注入,默认是属性注入。
1 2 3 4 5 6 7 8
| public enum InjectionStrategy {
FIELD,
CONSTRUCTOR }
|
类似如此使用
1
| @Mapper(componentModel = "cdi" injectionStrategy = InjectionStrategy.CONSTRUCTOR)
|
5.4 自定义类型转换
有时候,在对象转换的时候可能会出现这样一个问题,就是源对象中的类型是Boolean类型,而目标对象类型是String类型,这种情况可以通过@Mapper
的uses属性来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Data public class User { private Integer id; private String name; private String address; private Date birth; private Boolean isDisable; } @Data public class UserDto implements Serializable { private Integer id; private String name; private String address; private Date birth; private String isDisable; } @Mapper(uses = {BooleanStrFormat.class}) public interface UserMapper { UserDto userToNewUserDto(User user); } public class BooleanStrFormat { public String toStr(Boolean isDisable) { if (isDisable) { return "Y"; } else { return "N"; } } public Boolean toBoolean(String str) { if (str.equals("Y")) { return true; } else { return false; } } }
|
要注意的是,如果使用了例如像spring这样的环境,Mapper引入uses类实例的方式将是自动注入,那么这个类也应该纳入Spring容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test public void userPoToUserDto() { User user =new User(); user.setId(1); user.setName("myx"); user.setAddress("河北沧州"); user.setBirth(new Date()); user.setIsDisable(true); SubUser subUser =new SubUser(); subUser.setDeleted(0); subUser.setName("rkw"); UserMapper mapper = Mappers.getMapper(UserMapper.class); UserDto userDto = mapper.userToNewUserDto(user); System.out.println(userDto); }
|
6、最后说一句
如果这篇文章对你有所帮助,陈某码字不易,求一键三连:点赞、在看、转发,你的支持将是我写作的动力!
另外可以加陈某微信special_coder,做一个朋友圈点赞之交!