大多时候时候使用的是ApacheSpring``BeanUtils,今天,我们来看一下一个更高效的属性拷贝方式:BeanCopier

https://github.com/cglib/cglib

https://github.com/cglib/cglib/blob/master/cglib/src/main/java/net/sf/cglib/beans/BeanCopier.java

首先梳理出来现在有哪些对象拷贝的方式:

  1. Apache的BeanUtils:BeanUtils是Apache commens组件里面的成员,由Apache提供的一套开源api,用于简化对javaBean的操作,能够对基本类型自动转换。
  2. Spring的BeanUtils:BeanUtils是Spring框架提供的对Java反射和自省API的包装。其主要目的是利用反射机制对JavaBean的属性进行处理。
  3. Mapstruct:

MapStruct是一个Java 注释处理器,用于为Java Bean类生成类型安全和高性能的映射器。它使您不必手工编写映射代码,这是一个繁琐且容易出错的任务。该生成器具有合理的默认值和许多内置的类型转换,但是在配置或实现特殊行为时,它会自动退出。
与运行时工作的映射框架相比,MapStruct具有以下优点:

  • 通过使用普通方法调用而不是反射来快速执行
  • 编译时类型安全。只能映射彼此映射的对象和属性,因此不会将订单实体意外映射到客户DTO等。
  • 自包含代码 -没有运行时依赖项
  • 如果发生以下情况,则在构建时清除错误报告:
    • 映射不完整(并非所有目标属性都被映射)
    • 映射不正确(找不到正确的映射方法或类型转换)
  • 易于调试的映射代码(或手动编辑,例如在生成器中有错误的情况下)
    github mapstruct/mapstruct

4、BeanCopier:BeanCopier是Cglib包中的一个类,用于对象的复制。目标对象必须先实例化 而且对象必须要有setter方法。

其实有很多种方法进行属性拷贝的,例如dozer等等 下面看下测试性能吧:以:万级进行测试,我觉得Cglib太给力了.可以在遇到属性拷贝瓶颈时考虑.当然他们各有优点哈,功能也不尽相同.还需要多使用体会. 输出结果:手动Copy >Mapstuct>= cglibCopy > springBeanUtils > apachePropertyUtils > apacheBeanUtils 可以理解为: 手工复制 > cglib > 反射 > Dozer。

类型Framework 数据量 测试性能(10000调用次)time
Pure get/set   10ms
Cglib Beancopier   14ms
MapStruct   10ms
Easy mapper   46ms
Spring BeanUtils   96ms
Apache BeanUtils   249ms
Apache PropertieyUtils   130ms
Dozer   770ms

测试结果评估:

数据量 Apache Spring MapStruct BeanCopier
100w 391ms 250ms 45ms 57ms
10w 82ms 34ms 8ms 10ms
1w 30ms 19ms 2ms 7ms
1k 15ms 6ms 1ms 5ms
100 5ms 3ms 1ms 4ms
10 2ms 1ms 1ms 4ms

根据测试结果,我们可以得出在速度方面,MapStruct是最好的,执行速度是 Apache BeanUtils 的10倍、Spring BeanUtils 的 4-5倍、和 BeanCopier 的速度差不多。
由此可以看出,在大数据量级的情况下,MapStruct 和 BeanCopier 都有着较高的性能优势,其中 MapStruct 尤为优秀。如果你仅是在日常处理少量的对象时,选取哪个其实变得并不重要,但数据量大时建议还是使用MapStruct 或 BeanCopier 的方式,提高接口性能

一、背景

1.1 对象拷贝概念

Java中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括intdoublebytebooleanchar等简单数据类型,引用类型包括类、接口、数组等复杂类型。

对象拷贝分为浅拷贝(浅克隆)与深拷贝(深克隆)。

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

1.2 示例前准备

  • 源对象属性类UserDO.class(以下示例,源对象都用这个)
@Data
public class UserDO {private int id;private String userName;/*** 以下两个字段用户模拟自定义转换*/private LocalDateTime gmtBroth;private BigDecimal balance;public UserDO(Integer id, String userName, LocalDateTime gmtBroth, BigDecimal balance) {this.id = id;this.userName = userName;this.gmtBroth = gmtBroth;this.balance = balance;}
}
复制代码
  • 造数据工具类DataUtil.class
public class DataUtil {/*** 模拟查询出一条数据* @return*/public static UserDO createData() {return new UserDO(1, "Van", LocalDateTime.now(),new BigDecimal(100L));}/*** 模拟查询出多条数据* @param num 数量* @return*/public static List<UserDO> createDataList(int num) {List<UserDO> userDOS = new ArrayList<>();for (int i = 0; i < num; i++) {UserDO userDO = new UserDO(i+1, "Van", LocalDateTime.now(),new BigDecimal(100L));userDOS.add(userDO);}return userDOS;}
}
复制代码

一、对象拷贝之MapStruct(强烈推荐)

更多高级使用参照手册。

https://github.com/mapstruct

https://mapstruct.org/

   <!-- mapstruct start--><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-jdk8</artifactId><version>1.3.0.Final</version></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.3.0.Final</version><scope>provided</scope></dependency><!-- mapstruct end-->

转换:


import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;/*** * FileName: UserConvertUtils** @author: Van* Date:     2019-11-08 20:56* Description: ${DESCRIPTION}* Version: V1.0*/
@Mapper
public 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);/*** 多对一映射* @param userDO* @param userInfoDO* @return*/UserDTO doAndInfoToDto(UserDO userDO, UserInfoDO userInfoDO);
}

使用:

/*** * FileName: UserConvertUtilsDemo** @author: Van* Date:     2019-11-08 18:05* Description: ${DESCRIPTION}* Version: V1.0*/
@Slf4j
public class MapStructTest {/*** 一般拷贝*/@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);}/*** 包含类型转换的拷贝*/@Testpublic void doAndInfoToDto() {// 模拟查询出数据UserDO userDO = DataUtil.createData();log.info("拷贝前:userDO:{}", userDO);UserDTO userDTO = UserConvertUtils.INSTANCE.doAndInfoToDto(userDO, new UserInfoDO(1));log.info("拷贝后:userDTO:{}", userDTO);}}

二、对象拷贝之BeanUtils

ApacheSpring均有BeanUtils工具类, ApacheBeanUtils稳定性与效率都不行;SpringBeanUtils比较稳定,不会因为量大了,耗时明显增加,故一般都使用SpringBeanUtils

2.1 源码解读

Spring中的BeanUtils,其中实现的方式很简单,就是对两个对象中相同名字的属性进行简单get/set,仅检查属性的可访问性。

BeanUtils 源码

可以看到, 成员变量赋值是基于目标对象的成员列表, 并且会跳过ignore的以及在源对象中不存在的, 所以这个方法是安全的, 不会因为两个对象之间的结构差异导致错误, 但是必须保证同名的两个成员变量类型相同。

2.2 示例

@Slf4j
public class BeanUtilsDemo {public static void main(String[] args) {long start = System.currentTimeMillis();UserDO userDO = DataUtil.createData();log.info("拷贝前,userDO:{}", userDO);UserDTO userDTO = new UserDTO();BeanUtils.copyProperties(userDO,userDTO);log.info("拷贝后,userDO:{}", userDO);}
}
复制代码
  • 结果
18:12:11.734 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo - 拷贝前,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)
18:12:11.917 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo - 拷贝后,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)
复制代码

三、对象拷贝之BeanCopier

BeanCopier是用于在两个bean之间进行属性拷贝的。BeanCopier支持两种方式:

  1. 一种是不使用Converter的方式,仅对两个bean间属性名和类型完全相同的变量进行拷贝;
  2. 另一种则引入Converter,可以对某些特定属性值进行特殊操作。

3.1 基本使用

  • 依赖
<dependency><groupId>cglib</groupId><artifactId>cglib-nodep</artifactId><version>3.3.0</version>
</dependency>
复制代码

**注意:**该依赖非必须,因为Spring中已经集成了cglib,博主使用的就是org.springframework.cglib.beans.BeanCopier

3.1.1 属性名称、类型都相同

  • 目标对象属性类
@Data
public class UserDTO {private int id;private String userName;
}
复制代码
  • 测试方法
/*** 属性名称、类型都相同(部分属性不拷贝)*/
private static 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);
}
复制代码
  • 结果:拷贝成功
18:24:24.080 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:24:24.077, balance=100)
18:24:24.200 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userDTO:UserDTO(id=1, userName=Van)
复制代码

3.1.2 属性名称相同、类型不同

  • 目标对象属性类
@Data
public class UserEntity {private Integer id;private String userName;
}
复制代码
  • 测试方法
/*** 属性名称相同、类型不同*/
private static void sameNameDifferentType() {// 模拟查询出数据UserDO userDO = DataUtil.createData();log.info("拷贝前:userDO:{}", userDO);BeanCopier b = BeanCopier.create(UserDO.class, UserEntity.class, false);UserEntity userEntity = new UserEntity();b.copy(userDO, userEntity, null);log.info("拷贝后:userEntity:{}", userEntity);
}
复制代码
  • 结果
19:43:31.645 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:43:31.642, balance=100)
19:43:31.748 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userEntity:UserEntity(id=null, userName=Van)
复制代码
  • 分析

通过日志可以发现:UserDOint类型的id无法拷贝到UserEntityIntegerid

3.1.3 小节

BeanCopier只拷贝名称和类型都相同的属性。

即使源类型是原始类型(int, shortchar等),目标类型是其包装类型(Integer, ShortCharacter等),或反之:都不会被拷贝。

3.2 自定义转换器

通过3.1.2可知,当源和目标类的属性类型不同时,不能拷贝该属性,此时我们可以通过实现Converter接口来自定义转换器

3.2.1 准备

  • 目标对象属性类
@Data
public class UserDomain {private Integer id;private String userName;/*** 以下两个字段用户模拟自定义转换*/private String gmtBroth;private String balance;
}
复制代码

3.2.2 不使用Converter

  • 测试方法
/*** 类型不同,不使用Converter*/
public static void noConverterTest() {// 模拟查询出数据UserDO userDO = DataUtil.createData();log.info("拷贝前:userDO:{}", userDO);BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, false);UserDomain userDomain = new UserDomain();copier.copy(userDO, userDomain, null);log.info("拷贝后:userDomain:{}", userDomain);
}
复制代码
  • 结果
19:49:19.294 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:49:19.290, balance=100)
19:49:19.394 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userDomain:UserDomain(id=null, userName=Van, gmtBroth=null, balance=null)
复制代码
  • 分析

通过打印日志的前后对比,属性类型不同的字段id,gmtBroth,balance未拷贝。

3.2.3 使用Converter

  • 实现Converter接口来自定义属性转换
public  class UserConverter implements Converter {/*** 时间转换的格式*/DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");/*** 自定义属性转换* @param value 源对象属性类* @param target 目标对象里属性对应set方法名,eg.setId* @param context 目标对象属性类* @return*/@Overridepublic 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*/
public static void converterTest() {// 模拟查询出数据UserDO userDO = DataUtil.createData();log.info("拷贝前:userDO:{}", userDO);BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, true);UserConverter converter = new UserConverter();UserDomain userDomain = new UserDomain();copier.copy(userDO, userDomain, converter);log.info("拷贝后:userDomain:{}", userDomain);
}
复制代码
  • 结果:全部拷贝
19:51:11.989 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:51:11.985, balance=100)
19:51:12.096 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userDomain:UserDomain(id=1, userName=Van, gmtBroth=2019-11-02 19:51:11, balance=100)
复制代码

3.2.4 小节

  1. 一旦使用ConverterBeanCopier只使用Converter定义的规则去拷贝属性,所以在convert()方法中要考虑所有的属性。
  2. 但,使用Converter会使对象拷贝速度变慢。

3.3 BeanCopier总结

  1. 当源类和目标类的属性名称、类型都相同,拷贝没问题。
  2. 当源对象和目标对象的属性名称相同、类型不同,那么名称相同而类型不同的属性不会被拷贝。注意,原始类型(intshortchar)和 他们的包装类型,在这里都被当成了不同类型,因此不会被拷贝。
  3. 源类或目标类的settergetter少,拷贝没问题,此时setter多余,但是不会报错。
  4. 源类和目标类有相同的属性(两者的getter都存在),但是目标类的setter不存在,此时会抛出NullPointerException

四、BeanUtils与BeanCopier速度对比

废话不多说,我这里直接演示两种工具10000条数据拷贝的耗时对比

4.1 BeanUtils

  • 测试代码
private static void beanUtil() {List<UserDO> list = DataUtil.createDataList(10000);long start = System.currentTimeMillis();List<UserDTO> dtoList = new ArrayList<>();list.forEach(userDO -> {UserDTO userDTO = new UserDTO();BeanUtils.copyProperties(userDO,userDTO);dtoList.add(userDTO);});log.info("BeanUtils cotTime: {}ms", System.currentTimeMillis() - start);
}
复制代码
  • 结果(耗时:232ms)
20:14:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanUtils cotTime: 232ms
复制代码

4.2 BeanCopier

  • 测试代码
private static void beanCopier() {// 工具类生成10w条数据List<UserDO> doList = DataUtil.createDataList(10000);long start = System.currentTimeMillis();List<UserDTO> dtoList = new ArrayList<>();doList.forEach(userDO -> {BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);UserDTO userDTO = new UserDTO();b.copy(userDO, userDTO, null);dtoList.add(userDTO);});log.info("BeanCopier costTime: {}ms", System.currentTimeMillis() - start);
}
复制代码
  • 结果(耗时:116ms)
20:15:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier costTime: 116ms
复制代码

4.3 缓存BeanCopier实例提升性能

BeanCopier拷贝速度快,性能瓶颈出现在创建BeanCopier实例的过程中。 所以,把创建过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能。

  • 测试代码
private static 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);});log.info("BeanCopier 加缓存后 costTime: {}ms", System.currentTimeMillis() - start);}public static 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 static String genKey(Class<?> srcClazz, Class<?> destClazz) {return srcClazz.getName() + destClazz.getName();
}
复制代码
  • 结果(耗时:6ms)
20:32:31.405 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier 加缓存后 costTime: 6ms
复制代码

五、总结及源码

场景 耗时(10000次调用) 原理
BeanUtils 232ms 反射
BeanCopier 116ms 修改字节码
BeanCopier(加缓存) 6ms 修改字节码

Github 示例代码

推荐:BeanCopier 源码分析

其他框架:https://github.com/orika-mapper/orika

对象拷贝之Apache BeanUtils、Spring的BeanUtils、Mapstruct、BeanCopier、PropertieyUtils对比(深拷贝)相关推荐

  1. 对象拷贝 Apache BeanUtils与Spring BeanUtils性能比较

    前言 在我们实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息,比如DTO数据传输对象和数据对象DO,我们需要将DO对象进 ...

  2. 引用拷贝、对象拷贝、浅拷贝、深拷贝 到底是什么【详细例子介绍】

    首先要知道的: Java的数据类型分为基本数据类型和引用数据类型. 拷贝一个对象,可以使用 Cloneable接口的clone()方法. 对象拷贝分为 浅拷贝 和 深拷贝,这两种拷贝都是从引用拷贝 引 ...

  3. java实例拷贝,Apache Commons包 BeanUtils 对象拷贝实例demo

    Apache Commons包 BeanUtils 对象拷贝实例demo 发布时间:2019-05-08作者:laosun阅读(1846) Apache Commons包 BeanUtils 对象拷贝 ...

  4. 我在 Spring 的 BeanUtils 踩到的那些坑,千万不要犯!

    点击关注公众号,Java干货及时送达 转自:绝色天龙 链接:http://www.jianshu.com/p/357b55852efc 背景: 最近项目中在和第三方进行联调一个接口,我们这边发送htt ...

  5. 用Spring的BeanUtils前,建议你先了解这几个坑!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://h5ip.cn/q846 背景 最近项目中在和 ...

  6. spring boot 对象拷贝工具(Orika)

    1.spring 自带拷贝工具 LoginUser user = new LoginUser(); // 将data中字段相同的属性拷贝到user中BeanUtils.copyProperties(d ...

  7. 最近,我在Spring的BeanUtils踩了不少坑

    点击关注公众号,实用技术文章及时了解 来源:www.jianshu.com/p/357b55852efc 背景 最近项目中在和第三方进行联调一个接口,我们这边发送http请求给对方,然后接收对方的回应 ...

  8. Spring中BeanUtils 那些坑,千万不要犯!

    最近项目中在和第三方进行联调一个接口,我们这边发送http请求给对方,然后接收对方的回应,代码都是老代码.根据注释,对方的SDK中写好的Request类有一个无法序列化的bug,所以这边重新写了一个R ...

  9. Spring 的 BeanUtils 踩坑记,你是不是遇到过这些问题?

    背景 最近项目中在和第三方进行联调一个接口,我们这边发送http请求给对方,然后接收对方的回应,代码都是老代码.根据注释,对方的SDK中写好的Request类有一个无法序列化的bug,所以这边重新写了 ...

最新文章

  1. 《与编码人员一起工作》作者访谈
  2. linux 天堂测试软件,[Ubuntu] HTTP Live Streaming 安装测试
  3. iOS开发UI篇--UIScrollView思维导图[不断更新]
  4. 干货推荐 | 如何设计按钮?
  5. MongoDB(课时18 修改器)
  6. java的annotation_Java Annotation认知(包括框架图、详细介绍、示例说明)
  7. php gif 透明,解决PHP剪切缩略图生成png,gif透明图时,黑色背景问题
  8. 文件CRC和MD5校验
  9. 动态存储器是什么意思
  10. (5)呼吸灯systemverilog与VHDL编码
  11. Visual C++ 2008入门经典 第九章类的继承和虚函数(二)
  12. 【计算机网络笔记】物理层:概念传输媒体传输方式
  13. R语言信用评分卡:探索性数据分析
  14. c语言中的正号运算符,C语言 运算符
  15. 转载ios开发如何使用Xcode的Targets来管理开发和生产版本的构建
  16. 【晓松奇谈】历史是什么,人生是什么,世界是什么,未来是什么?高晓松的回答
  17. 特斯拉Q4财报:底部反弹70%,为信仰打call
  18. 电商详情页缓存架构(一)电商网站的商品详情页架构
  19. 在微型计算机中硬件和软件的关系是_,计算机的硬件系统和软件系统的关系是什么?...
  20. QUST日常训练(1)北极熊和大象

热门文章

  1. Photoshop CC 2018 Essential Training: The Basics Photoshop CC 2018基本培训:基础 Lynda课程中文字幕
  2. niagara向蓝图传递参数
  3. Moonbeam隆重推出您的个人开发小助手 — — Kapa.ai
  4. python中函数返回值是函数的函数的用法 func()()
  5. 物理化学复习【2】-- 活度标准态转换与活度相互作用系数
  6. 如果美工请假了,要程序员做图顶上!会是什么样的场景?经常我们都能听见程序员和设计师之间的互怼日常!
  7. python中的pypinyin库
  8. HAVING 的用法
  9. 利用VRV检查WPS软件的安装率
  10. JavaWeb(三)jQuery