秒懂Java之实体转化利器MapStruct详解-创新互联
文章目录[版权申明]非商业目的注明出处可自由转载
十多年专注成都网站制作,企业网站制作,个人网站制作服务,为大家分享网站制作知识、方案,网站设计流程、步骤,成功服务上千家企业。为您提供网站建设,网站制作,网页设计及定制高端网站建设服务,专注于企业网站制作,高端网页制作,对小搅拌车等多个行业,拥有丰富的网站设计经验。
出自:shusheng007
- 概述
- MapStruct
- 简介:
- 使用
- 如何配置
- 应用
- 进阶
- 装插件
- 调用方式
- 自定义映射
- 忽略映射
- 设置默认值
- 设置常量
- 数据类型转换
- 表达式
- 嵌套映射
- 集合映射
- 外部引用
- 多个数据源
- 切面操作
- 总结
- 源码
由于现代程序在追求扩展和维护性时很多采用分层的设计结构,所以在写程序时候需要在各种实体之间互相转换,而他们之间很多时候在业务或者技术架构上区别较大,在具体的属性上差别却很小。
例如将Programer
转换为ProgramerDto
就很普遍,如下所示:
public class Programer {
private String name;
private String proLang;
}
转换为:
public class ProgramerDto {
private String name;
private String proLang;
}
由于这些是繁琐易错且没有技术含量的编码工作,所以聪明的程序员就会寻求不断简化它的方法,MapStruct就是其中的一个利器。
MapStruct 简介:MapStruct is a Java annotation processor for the generation of type-safe and performant mappers for Java bean classes
大意就是:MapStruct 是一个用于Java的Bean的映射器,是它是基于注解的,而且是编译时APT(annotation processor tool)。不像其他APT是运行时,例如Spring里面的注解处理方式,是在运行时通过反射的方式处理的。
详细介绍可以到其官网查看:MapStruct源码,下面是官方给出的选择MapStruc的理由,你看看是否说服了你去使用它:
- Fast execution by using plain method invocations instead of reflection
- Compile-time type safety. Only objects and attributes mapping to each other can be mapped, so there’s no accidental mapping of an order entity into a customer DTO, etc.
- Self-contained code—no runtime dependencies
- Clear error reports at build time if:
- mappings are incomplete (not all target properties are mapped)
- mappings are incorrect (cannot find a proper mapping method or type conversion)
- Easily debuggable mapping code (or editable by hand—e.g. in case of a bug in the generator)
从前面的介绍我们得知,MapStruct是通过在编译时通过注解来生成代码的方式工作的,所以需要配置APT。此处我们还想使用lombok,所以也会顺便配置其与lombok结合的配置。
UTF-8 1.8 1.8 1.5.3.Final 1.18.20 0.2.0 org.mapstruct mapstruct${mapstruct.version} org.projectlombok lombok${lombok.version} provided ...
org.apache.maven.plugins maven-compiler-plugin 3.8.1 ${maven.compiler.target}
org.mapstruct mapstruct-processor ${mapstruct.version} org.projectlombok lombok ${lombok.version} org.projectlombok lombok-mapstruct-binding ${lombok-mapstruct-binding.version}
如上所示,主要配置了注解处理器:。如果不使用lombok的话,去掉相应的配置即可。
当完成了配置就就需要写代码了,主要是一些注解的使用,MapStruc提供的功能是很强大的,但是入门很容易的。
假设我们有如下两个需要转换的类:
@Data
public class Programer {
private String name;
private String lang;
private Double height;
private Date beDate;
private Address address;
private String girlName;
private String girlDes;
}
@Data
public class ProgramerDto {
private String name;
private String proLang;
private String height;
private String beDate;
private AddressDto address;
private GirlFriendDto girlFriend;
}
第一步: 定义一个interface
,使用@Mapper
标记
@Mapper
public interface ProgramerConvetor {
...
}
第二步:构建一个实例属性用于访问里面的方法。
@Mapper
public interface ProgramerConvetor {
ProgramerConvetor INSTANCE = Mappers.getMapper(ProgramerConvetor.class);
}
第三步:提供转换方法申明,必要时使用@Mapping
注解
@Mapper
public interface ProgramerConvetor {
ProgramerConvetor INSTANCE = Mappers.getMapper(ProgramerConvetor.class);
@Mapping(target = "lang", source = "proLang")
ProgramerDto toProgramerDto(Programer programer);
}
MapStruc默认会将两个bean的名称相同的属性进行映射,如果source与target的属性名称不一致则需要借助@Mapping
注解。
简单的转换就只需要以上3步就可以了,编译程序后就会在\target\generated-sources\annotations
下产生实现类了。
下面的代码是MapStruc自动生成的:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-01-08T16:51:05+0800",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
)
public class ProgramerConvetorImpl implements ProgramerConvetor {
@Override
public ProgramerDto toProgramerDto(Programer programer) {
if ( programer == null ) {
return null;
}
ProgramerDto programerDto = new ProgramerDto();
programerDto.setLang( programer.getProLang() );
programerDto.setName( programer.getName() );
...
return programerDto;
}
}
是不是和你手写的也差不多,那有了生成类我们就可以在代码中使用了:
public void runMap(){
Programer programer = new Programer();
programer.setName("shusheng007");
...
ProgramerDto programerDto = ProgramerConvetor.INSTANCE.toProgramerDto(programer);
log.info("dto: {}",programerDto);
}
可见,可以通过转换器接口里面的那个INSTANCE
实例属性来调用其方法。看是不是比你手写方便多了呢?特别是属性比较多,而其名称又有很多一致的情况下就更方便了。
前面那个是最基础的使用,MapStruc提供了非常灵活的映射方式,要完全掌握既没有必要又是不可能的,下面我们挑几个常用的以应对80%
的日常工作。
工欲善其事必先利其器,咱给Idea装上一个插件 MapStruct Support,各种代码智能提示走起来…
调用方式前面我们使用在接口中定义一个实例属性的方式来访问生成的方法,这有点不Spring,在Spring中我们习惯将bean交给Spring容器管理,MapSturc也支持。
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ProgramerConvetor {
}
通过修改@Mapper
的componentModel
的属性为spring即可。下面是生成的代码,发现已经添加了@Component
注解。
...
@Component
public class ProgramerConvetorImpl implements ProgramerConveto{
}
自定义映射当source与target里的属性名称不一致时需要显示指定映射关系
@Mapping(target = "lang", source = "proLang")
ProgramerDto toProgramerDto(Programer programer);
生成代码:
ProgramerDto programerDto = new ProgramerDto();
programerDto.setLang( programer.getProLang() );
忽略映射如果不想给ProgramerDto
的proLang
赋值可以忽略它。
@Mapping(target = "proLang", ignore = true)
ProgramerDto toProgramerDto(Programer programer);
设置默认值如果想实现在source值为null时给一个默认值也是可以了。
@Mapping(target = "proLang", defaultValue = "java")
ProgramerDto toProgramerDto(Programer programer);
生成代码:
ProgramerDto programerDto = new ProgramerDto();
if ( programer.getProLang() != null ) {
programerDto.setProLang( programer.getProLang() );
}
else {
programerDto.setProLang( "java" );
}
其实默认值不仅可以是一个具体的值,还可以是一个表达式,表达式一会我们再说。
设置常量给source的某个属性赋值为常量
@Mapping(target = "proLang", constant = "kotlin")
ProgramerDto toProgramerDto(Programer programer);
生成代码:
programerDto.setProLang( "kotlin" );
数据类型转换我们在进行bean映射的时候,有时会遇到数据类型不一致的情况。例如对于一个日期,source的数据类型是Date
,而target的数据类型是String
,这些情况怎么处理呢?
public class Programer {
private Double height;
private Date beDate;
}
public class ProgramerDto {
private String height;
private String beDate;
}
从上面的代码可以看到,我们的两个bean的数据类型是不一致的,但是MapStruct却可以帮我们自动转换
@Mapping(target = "height", source = "height")
ProgramerDto toProgramerDto(Programer programer);
生成的代码:
if ( programer.getHeight() != null ) {
programerDto.setHeight( String.valueOf( programer.getHeight() ) );
}
生成的代码已经将Double
帮我们转换成String
了。不仅如此,我们还可以对生成的字符串的格式进行设置,例如将身高数据保留两位小数
@Mapping(target = "height", source = "height" ,numberFormat = "#.00")
生成的代码:
if ( programer.getHeight() != null ) {
programerDto.setHeight( new DecimalFormat( "#.00" ).format( programer.getHeight() ) );
}
下面看一个日期相关的转换
@Mapping(target = "beDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
ProgramerDto toProgramerDto(Programer programer);
生成的代码:
if ( programer.getBeDate() != null ) {
programerDto.setBeDate( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ).format( programer.getBeDate() ) );
}
可见在日期转换中我们可以控制器转换后的格式。
表达式这个就比较厉害了,你就认为是方法调用即可,我喜欢。有时我们会遇到在做映射的时候不是简单的赋值,而是要进行计算,那这个功能就可以使用表达式来完成。
例如我们要实现将程序员名称变成大写个功能,就可以使用expression
这个属性进行配置。表达式的形式如下:java(代码调用)
。
@Mapping(target = "name", expression = "java(programer.getName().toUpperCase())")
ProgramerDto toProgramerDto(Programer programer);
生成代码:
programerDto.setName( programer.getName().toUpperCase() );
表达式里可以进行方法调用,例如上面的代码我们可以换一种方式写,将转换代码写成一个default函数。
注意这个defalut方法的签名一定要符合你的需求,因为MS会为每一个映射尝试这个方法,一旦符合了就会被使用,例如你写一个String 到 String的转换那就麻烦了,每个符合这个得属性转换都会用上…
@Mapping(target = "name", expression = "java(nameToUp(programer))")
ProgramerDto toProgramerDto(Programer programer);
default String nameToUp(Programer programer){
return Optional.ofNullable(programer)
.filter(Objects::nonNull)
.map(p->p.getName())
.orElse(null)
.toUpperCase();
}
对于分不清的情况所可以使用和Spring类似的方案,就是使用qualified。例如上面的功能还可以以下面的方案实现。
@Mapping(target = "name", qualifiedByName ={"nameToUp"} )
ProgramerDto toProgramerDto(Programer programer);
@Named("nameToUp")
default String nameToUp(String name) {
return name.toUpperCase();
}
嵌套映射我们经常会遇到bean里面套着bean的映射。
{
"name":"shusheng007",
"address":{
"country":"China",
"city":"TianJin"
}
}
对于这样的映射,我们只需要在mapper中提供一个嵌套bean的转换关系即可。
@Mapping(target = "address", source = "address")
ProgramerDto toProgramerDto(Programer programer);
//嵌套bean的转换关系
AddressDto toAddressDto(Address addr);
生成代码:
programerDto.setAddress( toAddressDto( programer.getAddress() ) );
@Override
public AddressDto toAddressDto(Address addr) {
if ( addr == null ) {
return null;
}
AddressDto addressDto = new AddressDto();
addressDto.setCountry( addr.getCountry() );
addressDto.setCity( addr.getCity() );
return addressDto;
}
其实MapStruct非常智能的,即使你不提供它也会尝试进行映射的。
集合映射只需要提供集合元素类型的映射即可。
AddressDto toAddressDto(Address addr);
List toAddressList(List addrList);
生成代码:
@Override
public AddressDto toAddressDto(Address addr) {
...
}
@Override
public List toAddressList(List addrList) {
if ( addrList == null ) {
return null;
}
List list = new ArrayList( addrList.size() );
for ( Address address : addrList ) {
list.add( toAddressDto( address ) );
}
return list;
}
外部引用上面我们介绍了表达式,通过它我们可以写代码逻辑,但是当转换关系需要调用外部类的方法时怎么办呢?我们有两种方法,下面看一下。
例如我们有如下要被引用的类:
@Component
public class GirlFriendMapper {
public GirlFriendDto toGirlFriendDto(Programer programer) {
GirlFriendDto girlFriendDto = new GirlFriendDto();
girlFriendDto.setName(programer.getName());
girlFriendDto.setDescription(programer.getGirlDes());
return girlFriendDto;
}
}
- 使用抽象类代替接口来做Mapper
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class ClzProgramerConvertor {
@Autowired
protected GirlFriendMapper girlFriendMapper;
...
@Mapping(target = "girlFriend", expression = "java(girlFriendMapper.toGirlFriendDto(programer))")
public abstract ProgramerDto toProgramerDto(Programer programer);
}
使用了抽象类后,你发现熟悉的味道回来了,可以使用@Autowired
随便往里面注入实例了,然后在expression
里面调用就好了,是不是很爽?
生成代码:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-01-08T21:03:05+0800",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
)
@Component
public class ClzProgramerConvertorImpl extends ClzProgramerConvertor {
@Override
public ProgramerDto toProgramerDto(Programer programer) {
ProgramerDto programerDto = new ProgramerDto();
...
programerDto.setGirlFriend( girlFriendMapper.toGirlFriendDto(programer) );
return programerDto;
}
protected AddressDto addressToAddressDto(Address address) {
...
}
}
那个girlFriendMapper
就是我们在父类中注入的。
- 使用
@Mapper
注解的import和use属性
import
属性就和java中的import
是一样的,导入后在expression中就可以不使用类的全限定名称了。例如你的转换用到了一个静态工具类,那么如果不在import中导入此工具类,那么使用的时候就要全限定名了。
@Mapping(target = "name", expression = "java(top.ss007.Util.toUpper(programer.getName()))")
当使用的映射方法在其他非静态类里时,就可以使用use
属性。
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,
uses = {
GirlFriendMapper.class
}
)
public interface ProgramerConvetor {
@Mapping(target = "girlFriend", source = "programer")
ProgramerDto toProgramerDto(Programer programer);
}
我们使用@Mapper
的use
属性将GirlFriendMapper引入进来。
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-01-08T21:03:17+0800",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
)
@Component
public class ProgramerConvetorImpl implements ProgramerConvetor {
@Autowired
private GirlFriendMapper girlFriendMapper;
@Override
public ProgramerDto toProgramerDto(Programer programer) {
ProgramerDto programerDto = new ProgramerDto();
...
programerDto.setGirlFriend( girlFriendMapper.toGirlFriendDto( programer ) );
return programerDto;
}
}
可见,引入的类的实例在实现类中被注入了。我们还可以通过injectionStrategy = InjectionStrategy.CONSTRUCTOR
指定通过构造函数来注入实例,如果不指定默认使用属性注入。
有时我们会遇到多个bean转一个bean的情况,需显示指定参数名称
@Mapping(target = "name", source = "programer.name")
@Mapping(target = "girlFriendName", source = "girl.name")
ProgramerDto toProgramerDto(Programer programer, Gir girl);
切面操作MapStruct 还提供了两个注解@BeforeMapping, @AfterMapping
用来实现在mapping前后的统一操作,这一般比较少用,但是在使用多态的时候还是很有作用的。
需求:我们有一个Human父类,有男人和女人两个子类,然后我们要将这两个子类型mapping成HumanDto。HumanDto中有个性别的属性,需要根据具体的类型决定。在mapping完成后,我们还要将名称修饰一下。
public class Human {
private String name;
}
public class Man extends Human{
}
public class Woman extends Human{
}
public class HumanDto {
private String name;
private GenderType genderType;
}
public enum GenderType {
MAN,WOMAN
}
当然我们可以写两个转换方法即可,一个Man到HumanDto,一个Woman到HumanDto,但是在使用的时候就比较麻烦了,需要传入具体的类型,代码也有重复。这种场景下我们就可以使用这两个注解完美的解决问题。
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class HumanConvertor {
@BeforeMapping
protected void humanDtoWithGender(Human human, @MappingTarget HumanDto humanDto) {
if (human instanceof Man) {
humanDto.setGenderType(GenderType.MAN);
} else if (human instanceof Woman) {
humanDto.setGenderType(GenderType.WOMAN);
}
}
@AfterMapping
protected void decorateName(@MappingTarget HumanDto humanDto) {
humanDto.setName(String.format("【%s】", humanDto.getName()));
}
public abstract HumanDto toHumanDto(Human human);
}
生成的代码:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2023-01-09T23:45:44+0800",
comments = "version: 1.5.3.Final, compiler: javac, environment: Java 11.0.16.1 (Oracle Corporation)"
)
@Component
public class HumanConvertorImpl extends HumanConvertor {
@Override
public HumanDto toHumanDto(Human human) {
...
HumanDto humanDto = new HumanDto();
//mapping前执行
humanDtoWithGender( human, humanDto );
humanDto.setName( human.getName() );
//mapping后执行
decorateName( humanDto );
return humanDto;
}
}
使用:
@Autowired
private HumanConvertor humanConvertor;
public void runHumanDemo(){
Human man = new Man();
man.setName("王二狗");
log.info("{}是大男人",humanConvertor.toHumanDto(man));
Human woman = new Woman();
woman.setName("牛翠华");
log.info("{}是小女人", humanConvertor.toHumanDto(woman));
}
总结至此,MapStruct的基本操作基本上都涉及到了,足以应对日常工作了,但是我还是那句话:那年我双手插兜,不知道什么叫… , 哎呀我去,跑偏了。 我还是那句话:MapStruct提供了大量的注解和自定义配置,如遇到特殊需求还需要去查看官方文档和示例。
忽忽悠悠又要过年啦,今年终于可以回老家过年了, 3年疫情终于要结束了,如果恶毒生活‘’强奸”了你,在反抗不了的情况下还是要学会隐忍和享受,保持乐观的心态,不断学习以待反抗之时,去追求美好生活…
源码源码请到首发文末查看:MapSturct
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
文章标题:秒懂Java之实体转化利器MapStruct详解-创新互联
文章地址:http://azwzsj.com/article/dgjsge.html