MapStruct从入门到出门(三)

790 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

映射集合,流

Collection类

MapStruct中也是支持Collection类型之间的映射,使用方法与正常Bean对象映射是一致的,只是多了个循环语句。

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface CollectionMapper {
​
    Set<Integer> stringsToIntegerSet(Set<String> strings);
​
    List<GoodsDto> goodsListToGoodsDtoList(List<Goods> goods);
​
    GoodsDto toGoodsDto(Goods goods);
}

经过MapStruct编译生成的实现类如下,可以看到确实相对于普通Bean对象映射就只是多了个循环语句。

@Component
public class CollectionMapperImpl implements CollectionMapper {
    public CollectionMapperImpl() {
    }
​
    public Set<Integer> stringsToIntegerSet(Set<String> strings) {
        if (strings == null) {
            return null;
        } else {
            Set<Integer> set = new LinkedHashSet(Math.max((int)((float)strings.size() / 0.75F) + 1, 16));
            Iterator var3 = strings.iterator();
​
            while(var3.hasNext()) {
                String string = (String)var3.next();
                set.add(Integer.parseInt(string));
            }
​
            return set;
        }
    }
​
    public List<GoodsDto> goodsListToGoodsDtoList(List<Goods> goods) {
        if (goods == null) {
            return null;
        } else {
            List<GoodsDto> list = new ArrayList(goods.size());
            Iterator var3 = goods.iterator();
​
            while(var3.hasNext()) {
                Goods goods1 = (Goods)var3.next();
                list.add(this.toGoodsDto(goods1));
            }
​
            return list;
        }
    }
​
    public GoodsDto toGoodsDto(Goods goods) {
        if (goods == null) {
            return null;
        } else {
            GoodsDto goodsDto = new GoodsDto();
            goodsDto.setId(goods.getId());
            goodsDto.setPrice(goods.getPrice());
            return goodsDto;
        }
    }
}

Map类

不止支持Collection接口类型的映射,也是支持Map接口类型的映射操作的。例如下面的示例代码,将一个Map<Long, LocalDateTime>转换为Map<String,String>类型。

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface MyMapMapping {
​
​
    @MapMapping(valueDateFormat = "yyyy-MM-dd hh:mm:ss")
    Map<String,String> toStringMap(Map<Long, LocalDateTime> source);
}
​

生成的实现类如下

@Component
public class MyMapMappingImpl implements MyMapMapping {
    private final DateTimeFormatter dateTimeFormatter_yyyy_MM_dd_hh_mm_ss_0396102240 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
​
    public MyMapMappingImpl() {
    }
​
    public Map<String, String> toStringMap(Map<Long, LocalDateTime> source) {
        if (source == null) {
            return null;
        } else {
            Map<String, String> map = new LinkedHashMap(Math.max((int)((float)source.size() / 0.75F) + 1, 16));
            Iterator var3 = source.entrySet().iterator();
​
            while(var3.hasNext()) {
                Map.Entry<Long, LocalDateTime> entry = (Map.Entry)var3.next();
                String key = (new DecimalFormat("")).format(entry.getKey());
                String value = this.dateTimeFormatter_yyyy_MM_dd_hh_mm_ss_0396102240.format((TemporalAccessor)entry.getValue());
                map.put(key, value);
            }
​
            return map;
        }
    }

@MapMapping注解分别拥有键值的numberFormatdateFormat的属性,使用方法同上一节讲的@Mapping是一致的。

  • keyDateFormat
  • valueDateFormat
  • keyNumberFormat
  • valueNumberFormat

接口集合的实现类型

对于映射方法的返回类型是集合的接口类型时,MapStruct将会选择其一个实现类来生成,具体生成类型如下表。

Interface typeImplementation type
IterableArrayList
CollectionArrayList
ListArrayList
SetLinkedHashSet
SortedSetTreeSet
NavigableSetTreeSet
MapLinkedHashMap
SortedMapTreeMap
NavigableMapTreeMap
ConcurrentMapConcurrentHashMap
ConcurrentNavigableMapConcurrentSkipListMap

枚举之间映射

MapStruct支持将一个枚举类型转换为另一个枚举类型,默认情况下同名的枚举常量之间可以直接互相映射,如果枚举的常量名不同则需要使用@ValueMapping注解来进行一些自定义映射操作。

例如下面定义两个枚举类型(不要在意合不合理拉)

// PayType.java
@Getter
public enum PayType {
​
    UN_PAY(0),PAYING(1),PAYED(2),EXPIRE(3),OTHER_FAILURE(4);
    private Integer type;
​
    PayType(Integer type) {
        this.type = type;
    }
}
​
// PayDtoType.java
@Getter
@ToString
public enum PayDtoType {
​
    UN_PAY(0),SUCCESS(1),FAILURE(2);
    private Integer type;
​
    PayDtoType(Integer type) {
        this.type = type;
    }
}
// PayMapper.java
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface PayMapper {
​
    @ValueMappings({
            @ValueMapping(source = "PAYING",target = "UN_PAY"),
            @ValueMapping(source = "PAYED",target = "SUCCESS"),
            @ValueMapping(source = "EXPIRE",target = "FAILURE"),
            @ValueMapping(source = "OTHER_FAILURE",target = "FAILURE")
    })
    PayDtoType toPayDtoType(PayType payType);
}
​
// MapStruct生成的PayMapperImpl.java@Component
public class PayMapperImpl implements PayMapper {
    public PayMapperImpl() {
    }
​
    public PayDtoType toPayDtoType(PayType payType) {
        if (payType == null) {
            return null;
        } else {
            PayDtoType payDtoType;
            switch (payType) {
                case PAYING:
                    payDtoType = PayDtoType.UN_PAY;
                    break;
                case PAYED:
                    payDtoType = PayDtoType.SUCCESS;
                    break;
                case EXPIRE:
                    payDtoType = PayDtoType.FAILURE;
                    break;
                case OTHER_FAILURE:
                    payDtoType = PayDtoType.FAILURE;
                    break;
                case UN_PAY:
                    payDtoType = PayDtoType.UN_PAY;
                    break;
                default:
                    throw new IllegalArgumentException("Unexpected enum constant: " + payType);
            }
​
            return payDtoType;
        }
    }
}
​

需要注意一点,如果源枚举类型的常量在目标枚举类型没有相同的名字,同时也没有使用@ValueMapping注解去指定映射的常量,那么编译将会出错。不过MapStruct还提供了MappingConstants.ANY_REMAININGMappingConstants.ANY_UNMAPPED这两种模式来处理源枚举类型剩余/未指定的常量将其映射到指定的默认值上

例如下面使用MappingConstants.ANY_REMAINING模式,将剩余的源枚举常量映射到指定的目标枚举常量上。

    @ValueMappings({
            @ValueMapping(source = "PAYING",target = "UN_PAY"),
            @ValueMapping(source = "PAYED",target = "SUCCESS"),
            @ValueMapping(source = MappingConstants.ANY_REMAINING,target = "FAILURE")
    })
    PayDtoType toPayDtoTypeV2(PayType payType);
public PayDtoType toPayDtoTypeV2(PayType payType) {
    if (payType == null) {
        return null;
    } else {
        PayDtoType payDtoType;
        switch (payType) {
            case PAYING:
                payDtoType = PayDtoType.UN_PAY;
                break;
            case PAYED:
                payDtoType = PayDtoType.SUCCESS;
                break;
            case EXPIRE:
            case OTHER_FAILURE:
            default:
                payDtoType = PayDtoType.FAILURE;
                break;
            case UN_PAY:
                payDtoType = PayDtoType.UN_PAY;
        }
​
        return payDtoType;
    }
}

自定义对象工厂

默认情况下,MapStruct在生成方法的返回对象时,是通过调用返回对象的无参构造方法来实现的,不过也可以自定义对象工厂来实现别的对象创建方式,例如下面的使用例子。

// DtoFactory.java
public class DtoFactory {
​
    public GoodsDto createGoodsDto(){
        GoodsDto goodsDto = new GoodsDto();
        goodsDto.setDescription("商户太懒拉,什么都没有写");
        return goodsDto;
    }
}
​
// GoodsMapper.java
@Mapper(uses = {DtoFactory.class})
public interface GoodsMapper {
​
    GoodsMapper INSTANCE = Mappers.getMapper(GoodsMapper.class);
​
    @Mapping(source = "goods.name",target = "goodsName")
    @Mapping(source = "goods.id",target = "id")
    GoodsDto toGoodsDto(Goods goods);
}
​
// GoodsMapperImpl.java
public class GoodsMapperImpl implements GoodsMapper {
    private final DtoFactory dtoFactory = new DtoFactory();
​
    public GoodsMapperImpl() {
    }
​
    public GoodsDto toGoodsDto(Goods goods) {
        if (goods == null) {
            return null;
        } else {
            GoodsDto goodsDto = this.dtoFactory.createGoodsDto();
            goodsDto.setGoodsName(goods.getName());
            goodsDto.setId(goods.getId());
            goodsDto.setPrice(goods.getPrice());
            return goodsDto;
        }
    }
}

通过@Mapper的uses属性来注册对象工厂,同时你也可以在对象工厂的方法上添加@ObjectFactory注解,这样就可以让源对象作为参数传入你的方法中

    @ObjectFactory
    public GoodsDto createGoodsDto(Goods goods){
        GoodsDto goodsDto = new GoodsDto();
        goodsDto.setDescription("商户太懒拉,什么都没有写");
        return goodsDto;
    }

自定义Naming Strategy

通常在Java中命名规则时是驼峰风格的,但有时因为祖传代码或者第三方接口对象等原因。定义的Bean对象并不总是驼峰风格的,而是下划线风格等,这个时候MapStruct默认的访问器命名策略就不能映射到了。除了手动一一声明@Mapping注解之外,还有什么操作吗?当然是有滴,MapStruct提供了org.mapstruct.ap.spi.AccessorNamingStrategy的SPI,从SPI的名字上也可以知道这是负责命名策略的。

创建processor模块

那么下面我们就来实现一下自己的NamingStrategy,以便不同风格的属性名也可以被自动映射到。

下面就新建一个项目,引入相关依赖,并实现自定义的SPI,随后创建META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy文件,里面填写实现类的全限定名。

    <dependencies>
        <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.8.2</version>
        </dependency>
    </dependencies>
public class CustomAccessorNamingStrategy extends DefaultAccessorNamingStrategy {
​
    @Override
    public void init(MapStructProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }
​
    @Override
    public String getPropertyName(ExecutableElement getterOrSetterMethod) {
        String propertyName = super.getPropertyName(getterOrSetterMethod);
        return NamingCase.toCamelCase(propertyName);
    }
}
​
// META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy
pers.orange.processor.CustomAccessorNamingStrategy

测试processor模块

随后我们引入该模块测试一下效果

        <path>
            <groupId>pers.orange</groupId>
            <artifactId>processor</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </path>
// Student.java
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Student {
​
    private Integer id;
    
    private String firstName;
    
    private String lastName;
}
​
// StudentDto.java
@Data
public class StudentDto {
​
    private Integer id;
​
    private String first_name;
​
    private String last_name;
}
​
// StudentMapper.java
@Mapper
public interface StudentMapper {
​
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
​
    StudentDto toStudentDto(Student student);
}
​
    @Test
    void testObjectFactory(){
        Student student = new Student(1,"zijia","chen");
        StudentDto studentDto = StudentMapper.INSTANCE.toStudentDto(student);
        log.info("Dto:{}",studentDto); // Dto:StudentDto(id=1, first_name=zijia, last_name=chen)
    }
​
​

可以看到现在不同命名风格的字段也能够自动匹配映射拉~。

小憩一下

这一节介绍了MapStruct是如何映射处理集合类、枚举类,以及自定义对象的创建方式和如何使用提供的SPI接口来实现自己的定制化需求。下一节就来介绍如何使用APT来实现一个简单的MapStruct的相关功能~

img