java对象拷贝工具测试及MapStruct使用

开发时碰到的问题,自己记录方便日后观看

一、工具

本次测试工具:

  1. set/get方法
  2. spring BeanUtil
  3. hutool BeanUtil
  4. fastjson序列化

二、测试

为了测试更加细致,本次测试分为三个方向进行测试,会使用几种组合进行测试。

  1. 对象复杂度
  2. 对象大小
  3. 对象数量

(一)场景一

这里有个坑详见:场景一:第一次测试(坑位)

  • 复杂对象
  • 小对象
  • 数据量:10个对象

(二)场景二

  • 复杂对象
  • 小对象
  • 数据量:10000个对象

数据量提升后,hutool工具效率明显比spring慢。在多次测试后,当数据量达到三位数时hutool工具效率低于spring,相反三位数前hutool>spring。json先不看后续会有其它结论

(三)场景三

  • 简单对象
  • 小对象
  • 数据量:10000个对象

对象变为简单对象后,性能得到提高。也就是说spring/hutool/json都会受到对象复杂度影响

(四)场景四

  • 简单对象
  • 大对象
  • 数据量:10000个对象

对象变为大对象后,spring/hutool不受影响,json则变慢。这块是因为copy对象用的是反射,而json是序列化,序列化的解析方式都会受到内容大小的影响。序列化的优化,简单记录后续学习调研使用:Protobuf、MessagePack

三、结论

  1. set/get方式无论何时都是最快的,原因:
    • 这种方式只是将上个对象的地址赋值给拷贝对象,不涉及额外操作
    • 这种方式注意下,因为是地址赋值,所以拷贝后的对象此内容变更会影响拷贝前的对象。也就是我们常说的浅拷贝
  2. hutool bean工具/spring bean工具/fastJson序列化工具都属于深拷贝
    • hutool/spring拷贝方式和性能消耗点
      • 反射:这就不多说了
      • 类型转换和检查:简单掰了下源码,对是否有set/get方法、源对象和目标对象属性方法是否私有、源类型是否可以赋值目标类型进行校验(ClassUtils.isAssignable)
    • json
      • JSON序列化/反序列化
  3. hutool工具在数据量达到三位数前是优于spring工具,达到三位数后随着数据量越大则变慢的很明显。
  4. 越复杂的对象处理效率越慢,10000个对象在100、200ms性能要求没那么极限的话也在可接受范围内
  5. 序列化拷贝方式受到对象大小的影响

四、坑位

(一)场景一:第一次测试(坑位)

此场景与本次性能测试无关,可跳过直接看后续,在此做个记录为以后判断问题使用。

  • 复杂对象
  • 小对象
  • 小数据量:10个对象

这块在测试时遇到了一个坑,可以看下图

10个对象spring、hutool、json竟然需要花费100多毫秒?这工具岂不是无法使用了?

问题分析及排查:

  • 我最初以为是我用的对象过于复杂导致的
  • 重新创建简单对象再次测试

  • 是快了一点点,但肯定不是根本原因
  • 于是把数据量从10改到1,看结果

  • 好家伙,也就是说1次的性能跟10次差不多。那问题其实就不是出自spring\hutool\json工具本身
  • 网上查询资料,结论就是:虚拟机(JVM)会对代码进行优化,但是这通常需要一些时间来"热身"。在短小的测试中,这些优化可能还没有充分发挥作用。
  • “热身”后在次测试,测试场景恢复为:复杂对象、小对象、对象数10

  • 之前还100多毫秒,现在瞬间降下来了

五、参考代码

StopWatch记录时间

for循环测试数据

@Test
    public void beanObjTest() {
        ArrayList<Obj1> obj1s = new ArrayList<>();
        StopWatch stopWatch = new StopWatch("耗时统计");

        stopWatch.start("初始化数据");
        for (int i = 0; i < 10000; i++) {
            Obj1 obj1 = new Obj1();
//            obj1.setText(i+"abc");
            obj1.setText(i+getStr());
            obj1s.add(obj1);
        }
        stopWatch.stop();

        stopWatch.start("set/get");
        List<Obj1> obj1List = new ArrayList<>();
        for (Obj1 obj1 : obj1s) {
            Obj1 obj1Set = new Obj1();
            obj1Set.setText(obj1.getText());
            obj1List.add(obj1Set);
        }
        stopWatch.stop();


        stopWatch.start("spring BeanUtil");
        List<Obj1> obj1Spring = new ArrayList<>();
        for (Obj1 obj1 : obj1s) {
            Obj1 obj1Copy = new Obj1();
            BeanUtils.copyProperties(obj1, obj1Copy);
            obj1Spring.add(obj1Copy);
        }
        stopWatch.stop();

        stopWatch.start("hutool BeanUtil");
        List<Obj1> obj1Hutool = new ArrayList<>();
        for (Obj1 obj1 : obj1s) {
            Obj1 obj1Copy = BeanUtil.copyProperties(obj1, Obj1.class);
            obj1Hutool.add(obj1Copy);
        }
        stopWatch.stop();


        stopWatch.start("json序列化");
        List<Obj1> obj1Json = new ArrayList<>();
        for (Obj1 obj1 : obj1s) {
            Obj1 obj1Copy = JSON.parseObject(JSON.toJSONString(obj1), Obj1.class);
            obj1Json.add(obj1Copy);
        }
        stopWatch.stop();

        System.out.println(stopWatch.prettyPrint());
        log.info("{}",stopWatch.prettyPrint());
    }

六、引入MapStruct

用BeanUtils.copyProperties可以简化代码复杂度,但是基于上述要求极致性能又希望代码能够尽量优雅,可以考虑使用MapStruct。

MapStruct是基于编译时生成实现类更加安全、高效

(一)maven

<!--1.5.0.Beta1之前不支持map结构-->
<mapstruct.version>1.5.1.Final</mapstruct.version>

<!--mapstruct-->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
</dependency>


<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
            <source>1.8</source>
            <target>1.8</target>
            <encoding>UTF-8</encoding>
            <annotationProcessorPaths>
                <path>
                    <groupId>org.mapstruct</groupId>
                    <artifactId>mapstruct-processor</artifactId>
                    <version>${mapstruct.version}</version>
                </path>
                <path>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok-mapstruct-binding</artifactId>
                    <version>0.2.0</version>
                </path>
                <path>
                    <groupId>org.projectlombok</groupId>
                    <artifactId>lombok</artifactId>
                    <version>${lombok.version}</version>
                </path>
            </annotationProcessorPaths>
        </configuration>
    </plugin>
</plugins>

(二)代码示例

使用公共方式进行转换:

BaseConverter:

import org.mapstruct.IterableMapping;
import org.mapstruct.Named;

import java.util.List;

public interface BaseConverter<T, E> {

    /**
     * T转E
     *
     * @param t
     * @return
     */
    @Named("toObjE")
    E toObjE(T t);

    /**
     * E转T
     *
     * @param e
     * @return
     */
    @Named("toObjT")
    T toObjT(E e);

    /**
     * T-List转E-List
     *
     * @param tList
     * @return
     */
    @IterableMapping(qualifiedByName = "toObjE")
    List<E> toListE(List<T> tList);

    /**
     * E-List转T-List
     *
     * @param eList
     * @return
     */
    @IterableMapping(qualifiedByName = "toObjT")
    List <T> toListT(List<E> eList);
}

TestConverter:

import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface TestConverter extends BaseConverter<Test, EsTest> {

    @Mapping(target = "pin", expression ="java(StringUtils.defaultString(esTest.getPin(),\"test\"))")
    Test convertTest(EsTest esTest);
}

特殊逻辑映射可自使用@Mapping映射实现:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import java.util.Map;

@Mapper(componentModel = "spring", uses = CustomDateMapper.class)
public interface SelectionOrderConverter {
    @Mapping(source = "id", target = "recordId")
    @Mapping(source = "order_id", target = "orderId")
    @Mapping(source = "create_at", target = "createAt")
    SelectionOrderEsEntity covertEsEntity(Map<String, String> selectionOrder);

    @Mapping(source = "recordId", target = "id")
    SelectionOrderEntity esCovertEntity(SelectionOrderEsEntity selectionOrderEs);
}

CustomDateMapper:

@Component
public class CustomDateMapper {
    public Date stringToDate(String date) {
        if (StringUtils.isNotBlank(date)) {
            return DateUtil.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        } else {
            return null;
        }
    }
}

调用示例:
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;

/**
 * @author ext.xuzhengyang5
 * @date 2024/2/19
 * @Description
 */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class ConverterTest {
    @Autowired
    TestConverter testConverter;

    @Test
    public void test() {
        Test test = new Test();
        test.setId(1L);
        test.setEnv("dev");
        test.setUpdateAt(new Date());
        test.setRepayMoney(new BigDecimal("1.333"));
        test.setYn(1);
        EsTest esTest = testConverter.toObjE(test);
        log.info("esTest:{}", JSON.toJSONString(esTest));
    }

    @Test
    public void expressionTest() {
        EsTest esTest = new EsTest();
        Test test = testConverter.convertTest(esTest);
        log.info("test:{}", JSON.toJSONString(test));
    }

    @Autowired
    SelectionOrderConverter selectionOrderConverter;

    @Test
    public void selectionOrderTest() {
        /*SelectionOrderEntity selectionOrderEntity = new SelectionOrderEntity();
        selectionOrderEntity.setOrderId(12L);
        selectionOrderEntity.setId(3L);*/
        HashMap<String, String> map = new HashMap<>(8);
        map.put("order_id", "12");
        map.put("id", "3");
        map.put("create_at", "2024-01-24 19:41:09");
        map.put("env", "admin");
        SelectionOrderEsEntity selectionOrderEsEntity = selectionOrderConverter.covertEsEntity(map);
        log.info("selectionOrderEsEntity:{}", JSON.toJSONString(selectionOrderEsEntity));
    }

}

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值