草庐IT

MapStruct的使用详解与常见用法介绍

compellingMan 2024-01-02 原文

项目转换DTO使用总结,常用技巧

概要

mapstruct在当前轻量级框架开发中的重点使用,@Named注解使用示例,@AfterMapping与@BeforeMapping注解的详细常见用法,在转换DTO时,与过去常用的beanUtil转换有高性能的转换优势,编译期自动生成的mapper实现类能够更加优雅的来实现各种隐式类型转换,以实现快速而又敏捷的开发,告别臃肿的手动get、set与类型的强转

引入

当前core-service引入版本如下

<properties>
    <mapstruct.version>1.2.0.Final</mapstruct.version>
</properties>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>${mapstruct.version}</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${mapstruct.version}</version>
</dependency>

基本使用

  • 使用@Mapper注解,声明映射器,可以是接口,或者抽象类
  • 使用@Mapping注解,实现灵活的字段映射,定制映射的规则

注入方式

  • 工厂方式
@Mapper
public interface ItemMapper {
    //使用工厂方法获取Mapper实例
    ItemMapper INSTANCE = Mappers.getMapper(ItemMapper.class);
    ItemDTO toDTO(Item item);
}
  • 依赖注入方式
@Mapper(componentModel = "spring")
public interface ItemMapper {
    ItemDTO toDTO(Item item);
}

简单映射

代码可以自动生成的Mapper接口,可以自动转换相同属性的字段,无需其他声明。

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface AwardMapper extends BaseMapper<AwardDTO, Award> {

}

需要注意的是:再不写任何转换代码的情况下,如果实体类中配置了其他关联的一对多或一对一的实体,DTO如果同样需要转换的话,就需要保证属性名一致才能正常转换。

自动类型转换

对于基础数据类型会进行自动隐式的转换如int、long、String,Integer、Long等;

//生成的实现类代码可以看出来
@Component
public class AssemblerImpl implements Assembler {
    @Override
    public ProductDTO toDTO(Product product) {
        if ( product == null ) {
            return null;
        }
        ProductDTO productDTO = new ProductDTO();
        if ( product.getProductId() != null ) {
            //String自动转int
            productDTO.setProductId( Integer.parseInt( product.getProductId() ) );
        }
        if ( product.getPrice() != null ) {
            //Long转String
            productDTO.setPrice( String.valueOf( product.getPrice() ) );
        }
        return productDTO;
    }
}

指定格式转换

查看mapping注解可以看到

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD})
public @interface Mapping {
    String target();

    String source() default "";

    String dateFormat() default "";

    String numberFormat() default "";
    
    ...
}

dateFormat与numberFormat即可用来进行转换操作

  • dateFormat用于日期格式转换
@Mapper(componentModel = "spring")
public interface Demo4Assembler {
    @Mapping(target = "saleTime", dateFormat = "yyyy-MM-dd HH:mm:ss")  //Date转换成String
    @Mapping(target = "validTime", dateFormat = "yyyy-MM-dd HH:mm")    //String转换成Date
    ProductDTO toDTO(Product product);
}
  • 日期格式转换可以直接在DTO中注解形式转换,比mapper更为便捷
	// 活动开始时间
	@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss",timezone="GMT+8")
	private Timestamp startTime;
	
	// 活动结束时间
	@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss",timezone="GMT+8")
	private Timestamp endTime;
  • numberFormat用于数字格式转换【不常用】
   private String price;
   private Integer stock;
	@Mapper(componentModel = "spring")
	public interface Demo3Assembler {
    @Mapping(target = "price", numberFormat = "#.00元") 
    @Mapping(target = "stock", numberFormat = "#个") 
    ProductDTO toDTO(Product product);
}

不同参数映射

  • 基本不同属性名转换,需要使用@Mappings注解进行指定转换,参数为数组,支持多个字段转换
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface DrawRecordDetailMapper extends BaseMapper<DrawRecordDetailDTO, DrawRecord> {
    @Override
    @Mappings({
            @Mapping(target = "chanceId", source = "participationChanceId")
    })
    DrawRecordDetailDTO toDto(DrawRecord entity);

} 
  • 对象属性提取和普通属性装载对象中
//将entity中activity实体中name属性装载至DrawRecordDetailDTO中activityName属性上
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface DrawRecordDetailMapper extends BaseMapper<DrawRecordDetailDTO, DrawRecord> {

    @Override
    @Mappings({
            @Mapping(target = "activityName", source = "activity.name")
    })
    DrawRecordDetailDTO toDto(DrawRecord entity);   
}

//将entity中活动规则属性,转换至ActivityDetailDTO中activityRule对象中
	@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
	public interface ActivityDetailMapper extends BaseMapper<ActivityDetailDTO, Activity> {
	    @Override
	    @Mappings({
	            @Mapping(target = "activityRule.voteOverlapAvailable", source = "voteOverlapAvailable"),
	            @Mapping(target = "activityRule.voteMultipleOnceAvailable", source = 		                                    "voteMultipleOnceAvailable")
	    })
    ActivityDetailDTO toDto(Activity entity);
}    

自定义转换器

  • 基础映射方式,mapper接口中直接引入即可(对数据接口要求高)

一个自定义映射器可以定义多个映射方法,匹配时,是以方法的入参和出参进行匹配的

如果绑定的映射中,存在多个相同的入参和出参方法,将会报错

使用样例:将list中多个code在映射时,转换为String用逗号隔开

定义映射器:

@Component
public class MediaServiceFormater {
    public String format(List<String> serviceCodeList) {
        return String.join(",", serviceCode);
    }
}

mapper接口绑定映射器 【uses = {MediaServiceFormater.class}】

@Mapper(componentModel = "spring", uses = {MediaServiceFormater.class,unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MediaDetailMapper extends BaseMapper<MediaDetailDTO, Media> {

}
  • 基于named注解实现【常用】

映射器中使用注解来定义转换方法,使用时,具体属性绑定方法;

定义映射器:

@Component
public class MapStructConverterUtil {
    private static final Logger log = LoggerFactory.getLogger(MapStructConverterUtil.class);

    public MapStructConverterUtil() {
    }

    @Named("getNormalImage")
    public String getNormalImage(String images) {
        if (StringUtils.isEmpty(images)) {
            return null;
        } else {
            try {
                if (images.contains("normal")) {
                    JSONObject imageJson = JSON.parseObject(images);
                    JSONObject mapJson = imageJson.getJSONObject("map");
                    JSONArray normalJson = mapJson.getJSONArray("normal");
                    Integer index = normalJson.getInteger(0);
                    JSONObject object = imageJson.getJSONArray("list").getJSONObject(index);
                    return object.getString("fileUrl");
                }
            } catch (Exception var7) {
                log.error("normal图片解析出错,原images = [{}]", images);
            }

            return null;
        }
    }
  }

使用时绑定映射器:【uses = {MapStructConverterUtil.class}】

@Mapper(componentModel = "spring", uses = {MapStructConverterUtil.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ActivityUserDetailMapper extends BaseMapper<ActivityUserDetailDTO, Activity> {

    @Override
    @Mappings({
            @Mapping(target = "img1", source = "images", qualifiedByName = "getNormalImage"),
            @Mapping(target = "img2", source = "images", qualifiedByName = "getBgImage"),
            @Mapping(target = "img3", source = "images", qualifiedByName = "getPosterImage")
    })
    ActivityUserDetailDTO toDto(Activity entity);

}
  • Map映射

mapStruct还支持map集合的转换,可以对map进行隐式转换,支持对Key与value进行隐式转换

例如:日期转字符串,字符串转日期;同样支持@name注解转换形式,或使用qualifiedBy指定一个转换类(类中默认使用相同入参出参来匹配)

@Mapper(componentModel = "spring", uses = {ConverterUtil.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface SourceTargetMapper {
   @MapMapping(keyQualifiedByName = "getValue", valueDateFormat = "dd.MM.yyyy")
   Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source);
}

复杂映射实现

多个字段映射到一个字段,单个字段快速特殊处理

  • 自定义字段表达式映射
    • 单个字段特殊转换处理
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface GroupDetailOrderMapper extends BaseMapper<GroupDetailOrderDTO, GroupDetailOrder> {

    @Override
    @Mappings({
            @Mapping(target = "userName", expression = "java(entity.getUserName() == null ? \"\" : new String(java.util.Base64.getDecoder().decode(entity.getUserName()), java.nio.charset.StandardCharsets.UTF_8))")
    })
    GroupDetailOrderDTO toDto(GroupDetailOrder entity);
}
  • 多个字段处理为一个字段
@Mapper(componentModel = "spring", imports = DecimalUtils.class) 
public interface Demo16Assembler {
    @Mapping(target = "price", expression = "java(product.getPrice1() + product.getPrice2())") 
    @Mapping(target = "price2", expression = "java(DecimalUtils.add(product.getPrice1(), product.getPrice2()))") 
    ProductDTO toDTO(Product product);
}
  • 前后置处理实现复杂映射

使用@BeforeMapping与@AfterMapping注解来实现,需要定义默认方法在mapper接口中【java8后支持】

使用前注意:

​ 在调用上下文上的映射方法之前/之后,不执行空检查参数。调用方需要确保在这种情况下不传递null;

@Mapper(componentModel = “spring”, uses = {MapStructConverterUtil.class, MediaConverterUtil.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MediaDetailMapper extends BaseMapper<MediaDetailDTO, Media> {

 	//前置执行
    @BeforeMapping 
    default MediaDetailDTO setMediaName(Media media) {
        MediaDetailDTO mediaDetailDTO = new MediaDetailDTO();
        mediaDetailDTO.setName("默认名称");
        return mediaDetailDTO;
    }
    
    @Override
    @Mappings({
            @Mapping(target = "img1", source = "images", qualifiedByName = "getNormalImage"),
            @Mapping(target = "img2", source = "images", qualifiedByName = "getBgImage"),
            @Mapping(target = "img3", source = "images", qualifiedByName = "getPosterImage"),
            @Mapping(target = "cp", source = "contentProviderId", qualifiedByName = "getCP"),
            @Mapping(target = "audioTrack", source = "audioDescription")
    })
    MediaDetailDTO toDto(Media media);

	 //后置执行
    @AfterMapping
    default void setMediaType(@MappingTarget MediaDetailDTO mediaDetailDTO, Media media) {
        Integer type = media.getType();
        Integer contentType = media.getContentType();
        String contentTyp = String.format("%02d", contentType);
        mediaDetailDTO.setMediaType(type + contentTyp);
    }
}

隐式映射实现

利用继承关系,继承mapper自动生成的实现类,来重写增强

此转换方式,同样可以实现在dto转换后后置执行,实体真正实现类中代码简洁也较为优雅

需要注意的是,一定要先实现父类转换方法拿到结果后在进行后续增强转换

super.toDto(entity)
实现样例如下:

//实现类引入方式使用@Resource注解引入
 @Autowired
 @Resource(name = "activityDetailMapperImplPlus")
 private ActivityDetailMapper activityDetailMapper;

@Component("activityDetailMapperImplPlus")
@RequiredArgsConstructor
public class ActivityDetailMapperImplPlus extends ActivityDetailMapperImpl {

    private final VoteTargetRepository voteTargetRepository;

    @Override
    public ActivityDetailDTO toDto(Activity entity) {
        ActivityDetailDTO activityDetailDTO = super.toDto(entity);
        
        Long activityDetailDTOId = activityDetailDTO.getId();
        Integer type = activityDetailDTO.getType();
        boolean vote = LocalActConstant.ACT_TYPE_VOTE == type;
        if (vote) {
            Optional<List<VoteTarget>> voteOp = this.voteTargetRepository.findByActivityIdAndStatus(activityDetailDTOId, 1);
            if (voteOp.isPresent()) {
                List<VoteTarget> voteTargets = voteOp.get();
                //累积投票
                activityDetailDTO.setTotalVoteNum(voteTargets.stream()
                        .filter(voteTarget -> voteTarget.getCurrentNumber() != null)
                        .mapToInt(VoteTarget::getCurrentNumber).sum());
            }
        }
        return activityDetailDTO;
    }
}

隐式转换扩展

如果实体类中,关联了其他一对多或多对多的引用类型,JPA在mapper层自动转换后,又需要对引用对象增加出参或出参数据结构转换时,这时候利用转换生成是实现类继承强化后,再绑定到mapper中即可。这样就无需在接口实现层进行循环处理

使用样例如下:

MarketingActivityDetailDTO中有 private List registrationEvents;

需要在BaseRegistrationEventDTO对象中增加出参

//报名人数
private Long totalRegister;
接口侧转换使用的MarketingActivityDetailMapper接口如下:引用必须要包含RegistrationEventSimpleMapperImplPlus.class

uses = {MapStructConverterUtil.class, TemplateParamValueMapper1.class, RegistrationEventSimpleMapperImplPlus.class}

@Mapper(componentModel = "spring", uses = {MapStructConverterUtil.class, TemplateParamValueMapper1.class, RegistrationEventSimpleMapperImplPlus.class}, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MarketingActivityDetailMapper extends BaseMapper<MarketingActivityDetailDTO, MarketingActivity> {

    @Override
    @Mappings({
            @Mapping(target = "img1", source = "images", qualifiedByName = "getNormalImage"),
            @Mapping(target = "img2", source = "images", qualifiedByName = "getBgImage"),
            @Mapping(target = "img3", source = "images", qualifiedByName = "getPosterImage")
    })
    MarketingActivityDetailDTO toDto(MarketingActivity entity);
}

RegistationEventSimpleMapper如下

@Component("registrationEventSimpleMapperImplPlus")
@RequiredArgsConstructor
public class RegistrationEventSimpleMapperImplPlus extends RegistrationEventSimpleMapperImpl {
    private final UserRegistrationRepository userRegistrationRepository;
    @Override
    public BaseRegistrationEventDTO toDto(RegistrationEvent entity) {
        BaseRegistrationEventDTO baseRegistrationEventDTO = super.toDto(entity);
        Long registrationEventId = baseRegistrationEventDTO.getId();
      baseRegistrationEventDTO.setTotalRegister(
         this.userRegistrationRepository.countByRegistrationEventId(registrationEventId));
        return baseRegistrationEventDTO;
    }
}

有关MapStruct的使用详解与常见用法介绍的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  7. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  8. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  9. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  10. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

随机推荐