关于java:Java对象转换方案分析与mapstruct实践

10次阅读

共计 8032 个字符,预计需要花费 21 分钟才能阅读完成。

简介:随着零碎模块分层一直细化,在 Java 日常开发中不可避免地波及到各种对象的转换,如:DO、DTO、VO 等等,编写映射转换代码是一个繁琐反复且还易错的工作,一个好的工具辅助,加重了工作量、晋升开发工作效率的同时还能缩小 bug 的产生

作者 | 久贤
起源 | 阿里技术公众号

一 前言

随着零碎模块分层一直细化,在 Java 日常开发中不可避免地波及到各种对象的转换,如:DO、DTO、VO 等等,编写映射转换代码是一个繁琐反复且还易错的工作,一个好的工具辅助,加重了工作量、晋升开发工作效率的同时还能缩小 bug 的产生。

二 罕用计划及剖析

1 fastjson

CarDTO entity = JSON.parseObject(JSON.toJSONString(carDO), CarDTO.class);

这种计划因为通过生成两头 json 格局字符串,而后再转化成指标对象,性能十分差,同时因为两头会生成 json 格局字符串,如果转化过多,gc 会十分频繁,同时针对简单场景反对能力有余,根本很少用。

2 BeanUtil 类

BeanUtil.copyProperties()联合手写 get、set,对于简略的转换间接应用 BeanUtil,简单的转换本人手工写 get、set。该计划的痛点就在于代码编写效率低、冗余繁冗还略显俊俏,并且 BeanUtil 因为应用了反射 invoke 去赋值性能不高。

只能适宜 bean 数量较少、内容不多、转换不频繁的场景。

apache.BeanUtils

org.apache.commons.beanutils.BeanUtils.copyProperties(do, entity);

这种计划因为用到反射的起因,同时自身设计问题,性能比拟差。团体开发规约明确规定禁止应用。

spring.BeanUtils

org.springframework.beans.BeanUtils.copyProperties(do, entity);

这种计划针对 apache 的 BeanUtils 做了很多优化,整体性能晋升不少,不过还是应用反射实现比不上原生代码解决,其次针对简单场景反对能力有余。

3 beanCopier

BeanCopier copier = BeanCopier.create(CarDO.class, CarDTO.class, false); 
copier.copy(do, dto, null);

这种计划动静生成一个要代理类的子类, 其实就是通过字节码形式转换成性能最好的 get 和 set 形式, 重要的开销在创立 BeanCopier,整体性能靠近原生代码解决,比 BeanUtils 要好很多,尤其在数据量很大时,然而针对简单场景反对能力有余。

4 各种 Mapping 框架

分类

Object Mapping 技术从大的角度来说分为两类,一类是运行期转换,另一类则是编译期转换:

  • 运行期反射调用 set/get 或者是间接对成员变量赋值。这种形式通过 invoke 执行赋值,实现时个别会采纳 beanutil, Javassist 等开源库。运行期对象转换的代表次要是 Dozer 和 ModelMaper。
  • 编译期动静生成 set/get 代码的 class 文件,在运行时间接调用该 class 的 set/get 办法。该形式实际上仍会存在 set/get 代码,只是不须要开发人员本人写了。这类的代表是:MapStruct,Selma,Orika。

剖析

  • 无论哪种 Mapping 框架,根本都是采纳 xml 配置文件 or 注解的形式供用户配置,而后生成映射关系。
  • 编译期生成 class 文件形式须要 DTO 依然有 set/get 办法,只是调用被屏蔽;而运行期反射形式在某些间接填充 field 的计划中,set/get 代码也能够省略。
  • 编译期生成 class 形式会有源代码在本地,不便排查问题。
  • 编译期生成 class 形式因为在编译期才呈现 java 和 class 文件,所以热部署会受到肯定影响。
  • 反射型因为很多内容是黑盒,在排查问题时,不如编译期生成 class 形式不便。参考 GitHub 上工程 java-object-mapper-benchmark 能够看出次要框架性能比拟。
  • 反射型调用因为是在运行期依据映射关系反射执行,其执行速度会显著降落 N 个量级。
  • 通过编译期生成 class 代码的形式,实质跟间接写代码区别不大,但因为代码都是靠模板生成,所以代码品质没有手工写那么高,这也会造成肯定的性能损失。

综合性能、成熟度、易用性、扩展性,mapstruct 是比拟优良的一个框架。

三 Mapstruct 使用指南

1 Maven 引入

2 简略入门案例

DO 和 DTO

这里用到了 lombok 简化代码,lombok 的原理也是在编译时去生成 get、set 等被简化的代码。

@Data 
public class Car {     
    private String make;     
    private int numberOfSeats;     
    private CarType type; 
}
@Data 
public class CarDTO {     
    private String make;     
    private int seatCount;     
    private String type; 
}

定义 Mapper

@Mapper 中形容映射,在编辑的时候 mapstruct 将会依据此形容生成实现类:

  • 当属性与其指标实体正本同名时,它将被隐式映射。
  • 当指标实体中的属性具备不同名称时,能够通过 @Mapping 正文指定其名称。
@Mapper 
public interface CarMapper {@Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); }

应用 Mapper

通过 Mappers 工厂生成动态实例应用。

@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);  

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}
Car car = new Car(...); 
CarDTO carDTO = CarMapper.INSTANCE.CarToCarDTO(car);

getMapper 会去 load 接口的 Impl 后缀的实现类。

通过生成 spring bean 注入应用,Mapper 注解加上 spring 配置,会主动生成一个 bean,间接应用 bean 注入即可拜访。

@Mapper(componentModel = "spring") 
public interface CarMapper {@Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}

主动生成的 MapperImpl 内容

如果配置了 spring bean 拜访会在注解上主动加上 @Component。

3 进阶应用

逆向映射

如果是双向映射,例如 从 DO 到 DTO 以及从 DTO 到 DO,正向办法和反向办法的映射规定通常是类似的,并且能够通过切换源和指标来简略地逆转。

应用注解 @InheritInverseConfiguration 批示办法应继承相应反向办法的反向配置。

@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);    

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car);    

    @InheritInverseConfiguration     
    Car CarDTOToCar(CarDTO carDTO); 
}

更新 bean 映射

有些状况下不须要映射转换产生新的 bean,而是更新已有的 bean。

@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    void updateDTOFromCar(Car car, @MappingTarget CarDTO carDTO);

汇合映射

汇合类型(List,Set,Map 等)的映射以与映射 bean 类型雷同的形式实现,即通过在映射器接口中定义具备所需源类型和指标类型的映射办法。MapStruct 反对 Java Collection Framework 中的多种可迭代类型。

生成的代码将蕴含一个循环,该循环遍历源汇合,转换每个元素并将其放入指标汇合。如果在给定的映射器或其应用的映射器中找到用于汇合元素类型的映射办法,则将调用此办法以执行元素转换,如果存在针对源元素类型和指标元素类型的隐式转换,则将调用此转换。

@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); 

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car);     

    List<CarDTO> carsToCarDtos(List<Car> cars);     

    Set<String> integerSetToStringSet(Set<Integer> integers);     

    @MapMapping(valueDateFormat = "dd.MM.yyyy")     
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source); 
}

编译时生成的实现类:

多个源参数映射

MapStruct 还反对具备多个源参数的映射办法。例如,将多个实体组合成一个数据传输对象。

在原案例新增一个 Person 对象,CarDTO 中新增 driverName 属性,依据 Person 对象取得。

@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "car.numberOfSeats", target = "seatCount")     
    @Mapping(source = "person.name", target = "driverName")     
    CarDTO CarToCarDTO(Car car, Person person); }

编译生成的代码:

默认值和常量映射

如果相应的源属性是 null,则能够指定默认值以将预约义值设置为指标属性。在任何状况下,都能够指定常量来设置这样的预约义值。默认值和常量被指定为字符串值。当指标类型是原始类型或装箱类型时,String 值将采纳字面量,在这种状况下容许位 / 八进制 / 十进制 / 十六进制模式,只有它们是无效的文字即可。在所有其余状况下,常量或默认值会通过内置转换或调用其余映射办法进行类型转换,以匹配指标属性所需的类型。

@Mapper 
public interface SourceTargetMapper {SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class);     

    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")     
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")     
    @Mapping(target = "stringConstant", constant = "Constant Value")     
    @Mapping(target = "integerConstant", constant = "14")     
    @Mapping(target = "longWrapperConstant", constant = "3001")     
    @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")     
    @Mapping(target = "stringListConstants", constant = "jack-jill-tom")     
    Target sourceToTarget(Source s); 
}

自定义映射办法或映射器

在某些状况下,可能须要手动实现 MapStruct 无奈生成的从一种类型到另一种类型的特定映射。

能够在 Mapper 中定义默认实现办法,生成转换代码将调用相干办法:

@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    @Mapping(source = "length", target = "lengthType")     
    CarDTO CarToCarDTO(Car car);     

    default String getLengthType(int length) {if (length > 5) {return "large";} else {return "small";}     
    } 
}

也能够定义其余映射器,如下案例 Car 中 Date 须要转换成 DTO 中的 String:

public class DateMapper {public String asString(Date date) {return date != null ? new SimpleDateFormat( "yyyy-MM-dd").format(date) : null;     
    }     

    public Date asDate(String date) {         
        try {return date != null ? new SimpleDateFormat( "yyyy-MM-dd").parse(date) : null;         
        } catch (ParseException e) {throw new RuntimeException( e);         
        }     
    } 
}
@Mapper(uses = DateMapper.class) 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}

编译生成的代码:

若遇到多个相似的办法调用时会呈现不置可否,需应用 @qualifiedBy 指定:

@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    @Mapping(source = "length", target = "lengthType", qualifiedByName = "newStandard")     
    CarDTO CarToCarDTO(Car car);     

    @Named("oldStandard")     
    default String getLengthType(int length) {if (length > 5) {return "large";} else {return "small";}     
    }     
    @Named("newStandard")     
    default String getLengthType2(int length) {if (length > 7) {return "large";} else {return "small";}     
    } 
}

表达式自定义映射

通过表达式,能够蕴含来自多种语言的构造。

目前仅反对 Java 作为语言。例如,此性能可用于调用构造函数,整个源对象都能够在表达式中应用。应留神仅插入无效的 Java 代码:MapStruct 不会在生成时验证表达式,但在编译期间生成的类中会显示谬误。

@Data 
@AllArgsConstructor 
public class Driver {     
    private String name;     
    private int age; 
}
@Mapper 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "car.numberOfSeats", target = "seatCount")     
    @Mapping(target = "driver", expression = "java( new com.alibaba.my.mapstruct.example4.beans.Driver(person.getName(), person.getAge()))")     
    CarDTO CarToCarDTO(Car car, Person person); 
} 

默认表达式是默认值和表达式的组合:

@Mapper(imports = UUID.class)
public interface SourceTargetMapper {SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class);     

    @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString())")     
    Target sourceToTarget(Source s); 
}

装璜器自定义映射

在某些状况下,可能须要自定义生成的映射办法,例如在指标对象中设置无奈由生成的办法实现设置的附加属性。

实现起来也很简略,用装璜器模式实现映射器的一个抽象类,在映射器 Mapper 中增加注解 @DecoratedWith 指向装璜器类,应用时还是失常调用。

@Mapper 
@DecoratedWith(CarMapperDecorator.class) 
public interface CarMapper {CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}
public abstract class CarMapperDecorator implements CarMapper {     
    private final CarMapper delegate;     
    protected CarMapperDecorator(CarMapper delegate) {this.delegate = delegate;}     
    @Override     
    public CarDTO CarToCarDTO(Car car) {CarDTO dto = delegate.CarToCarDTO(car);         
        dto.setMakeInfo(car.getMake() + "" + new SimpleDateFormat("yyyy-MM-dd").format(car.getCreateDate()));         
        return dto;     
    } 
}

原文链接
本文为阿里云原创内容,未经容许不得转载。

正文完
 0