020-47032205
当前位置:主页»新闻动态»ROR体育新闻»

面试官:你知道几种工具属性拷贝方式,说来看看?

文章出处:ROR体育 人气:发表时间:2021-11-23 06:16
本文摘要:当get/set太繁琐时;当BeanUtils无法拷贝荟萃时;当。可能,你需要好悦目看这篇文章。在做业务的时候,为了隔离变化,我们会将DAO查询出来的DO和对前端提供的DTO隔脱离来。或许90%的时候,它们的结构都是类似的;可是我们很不喜欢写许多冗长的b.setF1(a.getF1())这样的代码,于是我们需要简化工具拷贝方式。

ROR体育

当get/set太繁琐时;当BeanUtils无法拷贝荟萃时;当。可能,你需要好悦目看这篇文章。在做业务的时候,为了隔离变化,我们会将DAO查询出来的DO和对前端提供的DTO隔脱离来。或许90%的时候,它们的结构都是类似的;可是我们很不喜欢写许多冗长的b.setF1(a.getF1())这样的代码,于是我们需要简化工具拷贝方式。

一、配景1.1 工具拷贝观点Java中,数据类型分为值类型(基本数据类型)和引用类型,值类型包罗int、double、byte、boolean、char等简朴数据类型,引用类型包罗类、接口、数组等庞大类型。工具拷贝分为浅拷贝(浅克隆)与深拷贝(深克隆)。

浅拷贝与深拷贝差异分类浅拷贝深拷贝区别建立一个新工具,然后将当前工具的非静态字段复制到该新工具,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的工具。因此,原始工具及其副本引用同一个工具。建立一个新工具,然后将当前工具的非静态字段复制到该新工具,无论该字段是值类型的还是引用类型,都复制独立的一份。当你修改其中一个工具的任何内容时,都不会影响另一个工具的内容。

1.2 为什么需要拷贝工具Entity对应的是持久层数据结构(一般是数据库表的映射模型);Model 对应的是业务层的数据结构;VO 就是Controller和客户端交互的数据结构。例如:数据库查询出来的用户信息(表的映射模型)是UserDO,可是我们需要通报给客户端的是UserVO,这时候就需要把UserDO实例的属性一个一个赋值到UserVO实例中。在这些数据结构之间很大一部门属性都可能会相同,也可能差别。

1.3 有哪些拷贝方式org.springframework.beans.BeanUtils ;org.springframework.cglib.beans.BeanCopier;ma.glasnost.orika;org.mapstruct(强烈推荐)。二、BeanUtilsSpring中的BeanUtils,其中实现的方式很简朴,就是对两个工具中相同名字的属性举行简朴get/set,仅检查属性的可会见性。可以看到, 成员变量赋值是基于目的工具的成员列表, 而且会跳过ignore的以及在源工具中不存在的, 所以这个方法是宁静的, 不会因为两个工具之间的结构差异导致错误, 可是必须保证同名的两个成员变量类型相同。

2.1、单个工具拷贝我们把数据库查询出来的UserDO.java 拷贝到 UserVO.java。直接使用BeanUtils.copyProperties()方法。@Testpublic void commonCopy() { UserDO userDO = new UserDO(1L, "Van", 18, 1); UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); log.info("userVO:{}",userVO);}拷贝效果:.... userVO:UserVO(userId=1, userName=Van, age=18, sex=null)2.2、荟萃拷贝刚刚拷贝的是一个工具,可是有时候我们想拷贝一组UerDO.java,是一个荟萃的时候就不能这样直接赋值了。如果还根据这种逻辑,如下:@Testpublic void listCopyFalse() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 18, 2)); List<UserVO> userVOList = new ArrayList(); BeanUtils.copyProperties(userDOList, userVOList); log.info("userVOList:{}",userVOList);}拷贝效果:.... userVOList:[]通过日志可以发现,直接拷贝荟萃是无效的,那么怎么解决呢?2.3 暴力拷贝(不推荐)将需要拷贝的荟萃遍历,暴力拷贝。

@Testpublic void listCopyCommon() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = new ArrayList(); userDOList.forEach(userDO ->{ UserVO userVO = new UserVO(); BeanUtils.copyProperties(userDO, userVO); userVOList.add(userVO); }); log.info("userVOList:{}",userVOList);}拷贝效果:.... userVOList:[UserVO(userId=1, userName=Van, age=18, sex=null), UserVO(userId=2, userName=VanVan, age=20, sex=null)]虽然该方式可以解决,可是一点都不优雅,特别是写起来贫苦。2.4 优雅拷贝(本文推荐)通过JDK 8 的函数式接口封装org.springframework.beans.BeanUtils界说一个函数式接口函数式接口里是可以包罗默认方法,这里我们界说默认回调方法。@FunctionalInterfacepublic interface BeanUtilCopyCallBack <S, T> { /** * 界说默认回调方法 * @param t * @param s */ void callBack(S t, T s);}封装一个工具类 BeanUtilCopy.javapublic class BeanUtilCopy extends BeanUtils { /** * 荟萃数据的拷贝 * @param sources: 数据源类 * @param target: 目的类::new(eg: UserVO::new) * @return */ public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target) { return copyListProperties(sources, target, null); } /** * 带回调函数的荟萃数据的拷贝(可自界说字段拷贝规则) * @param sources: 数据源类 * @param target: 目的类::new(eg: UserVO::new) * @param callBack: 回调函数 * @return */ public static <S, T> List<T> copyListProperties(List<S> sources, Supplier<T> target, BeanUtilCopyCallBack<S, T> callBack) { List<T> list = new ArrayList<>(sources.size()); for (S source : sources) { T t = target.get(); copyProperties(source, t); list.add(t); if (callBack != null) { // 回调 callBack.callBack(source, t); } } return list; }}简朴拷贝测试 @Testpublic void listCopyUp() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new); log.info("userVOList:{}",userVOList);}拷贝效果:.... userVOList:[UserVO(userId=1, userName=Van, age=18, sex=null), UserVO(userId=2, userName=VanVan, age=20, sex=null)]通过如上方法,我们基本实现了荟萃的拷贝,可是从返回效果我们可以发现:属性差别的字段无法拷贝。

注意:UserDO.java 和UserVO.java 最后一个字段sex类型纷歧样,划分是:Integer/String优化一下新增性别枚举类public enum SexEnum { UNKNOW("未设置",0), MEN("男生", 1), WOMAN("女生",2), ; private String desc; private int code; SexEnum(String desc, int code) { this.desc = desc; this.code = code; } public static SexEnum getDescByCode(int code) { SexEnum[] typeEnums = values(); for (SexEnum value : typeEnums) { if (code == value.getCode()) { return value; } } return null; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public int getCode() { return code; } public void setCode(int code) { this.code = code; }}带特定转换的荟萃拷贝@Testpublic void listCopyUpWithCallback() { List<UserDO> userDOList = new ArrayList(); userDOList.add(new UserDO(1L, "Van", 18, 1)); userDOList.add(new UserDO(2L, "VanVan", 20, 2)); List<UserVO> userVOList = BeanUtilCopy.copyListProperties(userDOList, UserVO::new, (userDO, userVO) -> { // 这里可以界说特定的转换规则 userVO.setSex(SexEnum.getDescByCode(userDO.getSex()).getDesc()); }); log.info("userVOList:{}",userVOList);}拷贝效果:... userVOList:[UserVO(userId=1, userName=Van, age=18, sex=男生), UserVO(userId=2, userName=VanVan, age=20, sex=女生)]通过打印效果可以发现,UserDO.java 中Integer类型的sex复制到UserVO.java成了String类型的男生/女生。2.5 小结该方法是我们用的最多的方案,这里简朴封装下,可以利便荟萃类型工具的拷贝,平常使用基本够用,仅供参考。三、BeanCopierBeanCopier是用于在两个bean之间举行属性拷贝的。BeanCopier支持两种方式:一种是不使用Converter的方式,仅对两个bean间属性名和类型完全相同的变量举行拷贝;另一种则引入Converter,可以对某些特定属性值举行特殊操作。

3.1 通例使用@Testpublic void normalCopy() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); // 第一个参数:源工具, 第二个参数:目的工具,第三个参数:是否使用自界说转换器(下面会先容),下同 BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false); UserDTO userDTO = new UserDTO(); b.copy(userDO, userDTO, null); log.info("拷贝后:userDTO:{}", userDTO);}拷贝效果:...... 拷贝前:userDO:UserDO(id=1, userName=Van, sex=0, gmtBroth=2019-11-02T18:24:24.077, balance=100)...... 拷贝后:userDTO:UserDTO(id=1, userName=Van, sex=null)通过效果发现:UserDO的int类型的sex无法拷贝到UserDTO的Integer的sex。即:BeanCopier只拷贝名称和类型都相同的属性。纵然源类型是原始类型(int, short和char等),目的类型是其包装类型(Integer, Short和Character等),或反之:都不会被拷贝。

3.2 自界说转换器通过3.1可知,当源和目的类的属性类型差别时,不能拷贝该属性,此时我们可以通过实现Converter接口来自界说转换器目的工具属性类@Datapublic class UserDomain { private Integer id; private String userName; /** * 以下两个字段用户模拟自界说转换 */ private String gmtBroth; private String balance;}实现Converter接口来自界说属性转换public class UserDomainConverter implements Converter { /** * 时间转换的花样 */ DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** * 自界说属性转换 * @param value 源工具属性类 * @param target 目的工具里属性对应set方法名,eg.setId * @param context 目的工具属性类 * @return */ @Override public Object convert(Object value, Class target, Object context) { if (value instanceof Integer) { return value; } else if (value instanceof LocalDateTime) { LocalDateTime date = (LocalDateTime) value; return dtf.format(date); } else if (value instanceof BigDecimal) { BigDecimal bd = (BigDecimal) value; return bd.toPlainString(); } // 更多类型转换请自界说 return value; }}测试方法/** * 类型差别,使用Converter */@Testpublic void converterTest() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, true); UserDomainConverter converter = new UserDomainConverter(); UserDomain userDomain = new UserDomain(); copier.copy(userDO, userDomain, converter); log.info("拷贝后:userDomain:{}", userDomain);}拷贝效果:...... 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:51:11.985, balance=100)...... 拷贝后:userDomain:UserDomain(id=1, userName=Van, gmtBroth=2019-11-02 19:51:11, balance=100)注意一旦使用Converter,BeanCopier只使用Converter界说的规则去拷贝属性,所以在convert()方法中要思量所有的属性;毫无疑问,使用Converter会使工具拷贝速度变慢。3.3 缓存BeanCopier实例提升性能BeanCopier拷贝速度快,性能瓶颈泛起在建立BeanCopier实例的历程中。

所以,把建立过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能。测试代码@Testpublic void beanCopierWithCache() { List<UserDO> userDOList = DataUtil.createDataList(10000); long start = System.currentTimeMillis(); List<UserDTO> userDTOS = new ArrayList<>(); userDOList.forEach(userDO -> { UserDTO userDTO = new UserDTO(); copy(userDO, userDTO); userDTOS.add(userDTO); });}/** * 缓存 BeanCopier */private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIERS = new ConcurrentHashMap<>();public void copy(Object srcObj, Object destObj) { String key = genKey(srcObj.getClass(), destObj.getClass()); BeanCopier copier = null; if (!BEAN_COPIERS.containsKey(key)) { copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false); BEAN_COPIERS.put(key, copier); } else { copier = BEAN_COPIERS.get(key); } copier.copy(srcObj, destObj, null);}private String genKey(Class<?> srcClazz, Class<?> destClazz) { return srcClazz.getName() + destClazz.getName();}3.3 BeanCopier总结当源类和目的类的属性名称、类型都相同,拷贝没问题。当源工具和目的工具的属性名称相同、类型差别,那么名称相同而类型差别的属性不会被拷贝。

注意,原始类型(int,short,char)和 他们的包装类型,在这里都被当成了差别类型,因此不会被拷贝。源类或目的类的setter比getter少,拷贝没问题,此时setter多余,可是不会报错。源类和目的类有相同的属性(两者的getter都存在),可是目的类的setter不存在,此时会抛出NullPointerException。

加缓存可以提升拷贝速度。四、OrikaOrika 是 Java Bean 映射框架,可以实现从一个工具递归拷贝数据至另一个工具。

它的优点是:名字相同类型差别也能直接复制。4.1 所需依赖<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>1.5.4</version></dependency>4.2 映射工具类使用枚举实现的单例模式建立一个映射工具类,便于测试。public enum MapperUtils { /** * 实例 */ INSTANCE; /** * 默认字段工厂 */ private static final MapperFactory MAPPER_FACTORY = new DefaultMapperFactory.Builder().build(); /** * 默认字段实例 */ private static final MapperFacade MAPPER_FACADE = MAPPER_FACTORY.getMapperFacade(); /** * 默认字段实例荟萃 */ private static Map<String, MapperFacade> CACHE_MAPPER_FACADE_MAP = new ConcurrentHashMap<>(); /** * 映射实体(默认字段) * * @param toClass 映射类工具 * @param data 数据(工具) * @return 映射类工具 */ public <E, T> E map(Class<E> toClass, T data) { return MAPPER_FACADE.map(data, toClass); } /** * 映射实体(自界说设置) * * @param toClass 映射类工具 * @param data 数据(工具) * @param configMap 自界说设置 * @return 映射类工具 */ public <E, T> E map(Class<E> toClass, T data, Map<String, String> configMap) { MapperFacade mapperFacade = this.getMapperFacade(toClass, data.getClass(), configMap); return mapperFacade.map(data, toClass); } /** * 映射荟萃(默认字段) * * @param toClass 映射类工具 * @param data 数据(荟萃) * @return 映射类工具 */ public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data) { return MAPPER_FACADE.mapAsList(data, toClass); } /** * 映射荟萃(自界说设置) * * @param toClass 映射类 * @param data 数据(荟萃) * @param configMap 自界说设置 * @return 映射类工具 */ public <E, T> List<E> mapAsList(Class<E> toClass, Collection<T> data, Map<String, String> configMap) { T t = data.stream().findFirst().orElseThrow(() -> new ExceptionInInitializerError("映射荟萃,数据荟萃为空")); MapperFacade mapperFacade = this.getMapperFacade(toClass, t.getClass(), configMap); return mapperFacade.mapAsList(data, toClass); } /** * 获取自界说映射 * * @param toClass 映射类 * @param dataClass 数据映射类 * @param configMap 自界说设置 * @return 映射类工具 */ private <E, T> MapperFacade getMapperFacade(Class<E> toClass, Class<T> dataClass, Map<String, String> configMap) { String mapKey = dataClass.getCanonicalName() + "_" + toClass.getCanonicalName(); MapperFacade mapperFacade = CACHE_MAPPER_FACADE_MAP.get(mapKey); if (Objects.isNull(mapperFacade)) { MapperFactory factory = new DefaultMapperFactory.Builder().build(); ClassMapBuilder classMapBuilder = factory.classMap(dataClass, toClass); configMap.forEach(classMapBuilder::field); classMapBuilder.byDefault().register(); mapperFacade = factory.getMapperFacade(); CACHE_MAPPER_FACADE_MAP.put(mapKey, mapperFacade); } return mapperFacade; }}这个工具类中主要有四个方法:map(Class toClass, T data):普通的映射实体,主要映射名称相同(类型可以差别)的字段;map(Class toClass, T data, Map<String, String> configMap):自界说设置的映射,映射名称差别时,自界说映射对应名称;mapAsList(Class toClass, Collection data):普通的荟萃的映射;mapAsList(Class toClass, Collection data, Map<String, String> configMap):自界说的荟萃映射,自界说映射对应名称。

4.3 简朴测试拷贝名称相同类型可差别的属性@Testpublic void normalCopy() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); // 第一个参数:源工具, 第二个参数:目的工具,第三个参数:是否使用自界说转换器(下面会先容),下同 UserDTO userDTO = MapperUtils.INSTANCE.map(UserDTO.class, userDO);; log.info("拷贝后:userDTO:{}", userDTO);}字段名称差别,带翻译@Testpublic void converterTest() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); Map<String, String> config = new HashMap<>(); // 自界说设置(balance 转 balances) config.put("balance", "balances"); log.info("拷贝前:userDO:{}", userDO); UserDomain userDomain = MapperUtils.INSTANCE.map(UserDomain.class, userDO, config); log.info("拷贝后:userDomain:{}", userDomain);}拷贝荟萃@Testpublic void beanCopierWithCache() { List<UserDO> userDOList = DataUtil.createDataList(3); log.info("拷贝前:userDOList:{}", userDOList); List<UserDTO> userDTOS = MapperUtils.INSTANCE.mapAsList(UserDTO.class,userDOList); log.info("拷贝后:userDTOS:{}", userDTOS);}五、MapStructMapStruct 是一个自动生成 bean 映射类的代码生成器。MapStruct 还能够在差别的数据类型之间举行转换。

5.1 所需依赖mapstruct-jdk8包罗所需的注释,例如@Mapping。<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>1.3.0.Final</version></dependency>mapstruct-processor在编译,生成映射器实现的注释处置惩罚器。

<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.3.0.Final</version> <scope>provided</scope></dependency>5.2 如何使用?您所要做的就是界说一个mapper接口,该接口声明任何所需的映射方法。在编译期间,MapStruct将生成此接口的实现。

此实现使用普通的Java方法挪用来在源工具和目的工具之间举行映射。建立Mapper使用@Mapper注解标注该接口/抽象类是被MapStruct自动映射的,只有存在该注解才会将内部的接口方法自动实现。获取MapperMapStruct为我们提供了多种的获取Mapper的方式,习习用默认设置:接纳Mappers通过动态工厂内部反射机制完成Mapper实现类的获取。UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class);完整的一个转换器demo:@Mapperpublic interface UserConvertUtils { UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class); /** * 普通的映射 * * @param userDO UserDO数据持久层类 * @return 数据传输类 */ UserDTO doToDTO(UserDO userDO); /** * 类型转换的映射 * * @param userDO UserDO数据持久层类 * @return 数据传输类 */ @Mappings({ @Mapping(target = "gmtBroth", source = "gmtBroth", dateFormat = "yyyy-MM-dd HH:mm:ss"), @Mapping(target = "balances", source = "balance"), }) UserDTO doToDtoWithConvert(UserDO userDO);}测试/** * 一般拷贝 */@Testpublic void normalCopy() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); UserDTO userDTO = UserConvertUtils.INSTANCE.doToDTO(userDO); log.info("拷贝后:userDTO:{}", userDTO);}/** * 包罗类型转换的拷贝 */@Testpublic void doToDtoWithConvert() { // 模拟查询出数据 UserDO userDO = DataUtil.createData(); log.info("拷贝前:userDO:{}", userDO); UserDTO userDTO = UserConvertUtils.INSTANCE.doToDtoWithConvert(userDO); log.info("拷贝后:userDTO:{}", userDTO);}打印映射效果一般拷贝:...拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2020-04-21T21:38:39.376, balance=100)...拷贝后:userDTO:UserDTO(id=1, userName=Van, gmtBroth=2020-04-21T21:38:39.376, balances=null)包罗类型转换的拷贝:...拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2020-04-21T21:05:19.282, balance=100)...拷贝后:userDTO:UserDTO(id=1, userName=Van, gmtBroth=2020-04-21 21:05:19, balances=100)通过打印效果可以发现:相较于前者,包罗类型转换的拷贝可以自界说转换属性和时间花样等。

5.3 MapStruct 注解的关键词@Mapper:只有在接口加上这个注解, MapStruct 才会去实现该接口;@Mappings:设置多个@Mapping;@Mapping:属性映射,若源工具属性与目的工具名字一致,会自动映射对应属性:source:源属性;target:目的属性;dateFormat:字符串与日期之间相互转换;ignore: 忽略这个,某个属性不想映射,可以加个 ignore=true;5.4 多对一MapStruct 可以将几种类型的工具映射为另外一种类型,好比将多个 DO 工具转换为 DTO。详见:UserDTO doAndInfoToDto(UserDO userDO, UserInfoDO userInfoDO);5.5 为什么要用 MapStruct与手工编写映射代码相比,MapStruct通过生成繁琐且易于编写的代码来节约时间。遵循约定优于设置方法,MapStruct使用合理的默认值,但在设置或实现特殊行为时会接纳措施。

与动态映射框架相比,MapStruct具有以下优势:通过使用普通方法挪用而不是反射来快速执行编译时类型宁静:只能映射相互映射的工具和属性,不会将订单实体意外映射到客户DTO等。在构建时清除错误陈诉,如果映射不完整(并非所有目的属性都已映射)映射不正确(找不到合适的映射方法或类型转换)六、更多通过四种属性拷贝的方式,加上自己手动get/set,仅给出以下建议:简朴拷贝直接使用get/set;属性值过多的拷贝且已经使用Spring的情况下,使用BeanUtils;属性拷贝比力贫苦,存在转译且对拷贝速度有要求时使用MapStruct(性能险些等同于直接get/set)。


本文关键词:面试,官,你,知道,几种,工具,属性,拷贝,方式,ROR体育APP官网

本文来源:ROR体育-www.zh-express.cn

同类文章排行

最新资讯文章

Copyright © 2002-2021 www.zh-express.cn. ROR体育科技 版权所有  http://www.zh-express.cn  XML地图  ROR体育-APP官网