百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Java对象复制系列三: 手把手带你写一个 Spring BeanUtils

liebian365 2024-10-22 15:40 32 浏览 0 评论

前言

上一节我带大家手写了一个 Apache BeanUtils,并阅读了源码,大家应该都自己去阅读过源码了吧?

今天我大家手写一个 Spring BeanUtils,不像 Apache BeanUtils 我们就写了一个简单的案例,这次我们要写一个完整的 Spring BeanUtils 并带上和 Apache BeanUtils ,看看我们自己写的和 Apache BeanUtils 性能会有啥差别?

最佳实践

直接上案例

案例地址GitHub: https://github.com/zhuangjiaju/easytools/blob/main/easytools-test/src/test/java/com/github/zhuangjiaju/easytools/test/demo/beanutils/SpringBeanUtilsTest.java 案例地址gitee: https://gitee.com/zhuangjiaju/easytools/blob/main/easytools-test/src/test/java/com/github/zhuangjiaju/easytools/test/demo/beanutils/SpringBeanUtilsTest.java

需要上一期的教程自己去主页翻。

简单的复制对象

直接上代码:

/**
 * Spring BeanUtils 使用的demo
 */
@Test
public void demo() throws Exception {
    BeanUtilsDemoDTO beanUtilsDemo = new BeanUtilsDemoDTO();
    beanUtilsDemo.setId("id");
    beanUtilsDemo.setFirstName("firstName");

    BeanUtilsDemoDTO newBeanUtilsDemo = new BeanUtilsDemoDTO();
    //这里注意 spring 和 apache 的参数位置不一样 spring 是先 source 对象 后 target 对象
    BeanUtils.copyProperties(beanUtilsDemo, newBeanUtilsDemo);
    log.info("newBeanUtilsDemo: {}", newBeanUtilsDemo);
}

输出结果:

19:47:02.946 [main] INFO com.github.zhuangjiaju.easytools.test.demo.beanutils.SpringBeanUtilsTest -- newBeanUtilsDemo: BeanUtilsDemoDTO(id=id, firstName=firstName, lastName=null, age=null, email=null, phoneNumber=null, address=null, city=null, state=null, country=null, major=null, gpa=null, department=null, yearOfStudy=null, advisorName=null, enrollmentStatus=null, dormitoryName=null, roommateName=null, scholarshipDetails=null, extracurricularActivities=null)

可见已经复制对象成功了,输出里面有 firstName 的值。这里要注意 spring 和 apache 的参数位置不一样 spring 是先 source 对象 ,后 target 对象。

自己写一个完整的 Spring BeanUtils

Spring BeanUtils 源码相对比较简单,我们这里直接实现全部逻辑了,自己实现一遍在去看源码就会感觉怎么这么简单。

/**
 * 自己写一个完整的 Spring BeanUtils
 */
@Test
public void custom() throws Exception {
    BeanUtilsDemoDTO beanUtilsDemo = new BeanUtilsDemoDTO();
    beanUtilsDemo.setId("id");
    beanUtilsDemo.setFirstName("firstName");

    BeanUtilsDemoDTO newBeanUtilsDemo = new BeanUtilsDemoDTO();
    MySpringBeanUtils.copyProperties(beanUtilsDemo, newBeanUtilsDemo);
    log.info("newBeanUtilsDemo: {}", newBeanUtilsDemo);
}

我们自己实现的工具类:

前置知识:

Introspector.getBeanInfo: 是 Java 自带的一个类,可以获取一个类的 BeanInfo 信息,然后获取属性的描述资料 PropertyDescriptor BeanInfo : bean 的描述信息 PropertyDescriptor: bean 的属性的资料信息 ,可以获取到属性的 get/set 方法 Method: 方法,用这个对象可以反射掉调用

    public static class MySpringBeanUtils {

    private static final ConcurrentMap<Class<?>, DescriptorCache> STRONG_CLASS_CACHE
        = new ConcurrentHashMap<>();

    /**
     * 复制参数
     *
     * @param source
     * @param target
     * @throws Exception
     */
    public static void copyProperties(Object source, Object target) throws Exception {

        // 去缓存 获取目标对象的 PropertyDescriptor 属性资料
        DescriptorCache targetDescriptorCache = getPropertyDescriptorFormClass(target.getClass());
        // 去缓存 获取来源对象的 PropertyDescriptor 属性资料
        DescriptorCache sourceDescriptorCache = getPropertyDescriptorFormClass(source.getClass());

        // 循环目标对象
        for (PropertyDescriptor targetPropertyDescriptor : targetDescriptorCache.getPropertyDescriptors()) {
            // 获取属性名
            String name = targetPropertyDescriptor.getName();
            // 去Map 直接获取来源对象的属性资料 不用循环获取
            PropertyDescriptor sourcePropertyDescriptor = sourceDescriptorCache.getPropertyDescriptorMap().get(
                name);
            // 反射直接调用方法
            targetPropertyDescriptor.getWriteMethod().invoke(target,
                sourcePropertyDescriptor.getReadMethod().invoke(source));
        }
    }

    /**
     * 我们在这里加一个缓存 这样不用每次都去解析 PropertyDescriptor
     * @param beanClass
     * @return
     * @throws Exception
     */
    private static DescriptorCache getPropertyDescriptorFormClass(Class<?> beanClass) throws Exception {
        return STRONG_CLASS_CACHE.computeIfAbsent(beanClass, clazz -> {
            try {
                // 解析 PropertyDescriptor
                PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(beanClass,
                    Object.class).getPropertyDescriptors();
                // 转换成name的map 时间复杂度由 O(n) -> O(1) 也是用于加速
                Map<String, PropertyDescriptor> propertyDescriptorMap = Arrays.stream(propertyDescriptors)
                    .collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity(),
                        (oldValue, newValue) -> newValue));
                return DescriptorCache.builder()
                    .propertyDescriptors(propertyDescriptors)
                    .propertyDescriptorMap(propertyDescriptorMap)
                    .build();
            } catch (IntrospectionException ignore) {

            }
            return null;
        });
    }

}

缓存对象


@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public static class DescriptorCache {
    /**
     * 存储所有的 PropertyDescriptor 属性资料
     */
    private PropertyDescriptor[] propertyDescriptors;

    /**
     * 存储对应方法的  PropertyDescriptor 属性资料 用于加速
     */
    private Map<String, PropertyDescriptor> propertyDescriptorMap;

}

这个和上一期的 Apache BeanUtils 我们加入了缓存,速度有了质的飞跃。

其实Spring BeanUtils 实现基本我们实现的一模一样,特别的容易。

性能测试

性能测试的代码 我就不贴了,大家有兴趣的 可以直接看案例代码。


1次

100次

1000次

10000次

100000次

Apache BeanUtils

33ms

17ms

52ms

264ms

1964ms

自己写的BeanUtils

5ms

12ms

18ms

13ms

116ms

直接get/set

0ms

1ms

1ms

1ms

13ms

发现了没有 我们自己几行代码就实现了一个性能秒杀 Apache BeanUtils 的 BeanUtils。

很多源码没有大家想象的那么复杂,只要多去看看就行。

源码解析

org.apache.commons.beanutils.BeanUtils.copyProperties org.springframework.beans.BeanUtils.copyProperties(java.lang.Object, java.lang.Object, java.lang.Class<?>, java.lang.String...)

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
    @Nullable String... ignoreProperties) throws BeansException {

    Assert.notNull(source, "Source must not be null");
    Assert.notNull(target, "Target must not be null");

    Class<?> actualEditable = target.getClass();
    if (editable != null) {
        if (!editable.isInstance(target)) {
            throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
                "] not assignable to editable class [" + editable.getName() + "]");
        }
        actualEditable = editable;
    }
    // 这里去缓存里面获取 PropertyDescriptor
    PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    Set<String> ignoredProps = (ignoreProperties != null ? new HashSet<>(Arrays.asList(ignoreProperties)) : null);
    CachedIntrospectionResults sourceResults = (actualEditable != source.getClass() ?
        CachedIntrospectionResults.forClass(source.getClass()) : null);

    // 循环目标的 PropertyDescriptor
    for (PropertyDescriptor targetPd : targetPds) {
        Method writeMethod = targetPd.getWriteMethod();
        if (writeMethod != null && (ignoredProps == null || !ignoredProps.contains(targetPd.getName()))) {
            PropertyDescriptor sourcePd = (sourceResults != null ?
                sourceResults.getPropertyDescriptor(targetPd.getName()) : targetPd);
            if (sourcePd != null) {
                Method readMethod = sourcePd.getReadMethod();
                if (readMethod != null) {
                    if (isAssignable(writeMethod, readMethod, sourcePd, targetPd)) {
                        try {
                            ReflectionUtils.makeAccessible(readMethod);
                            // 读取到source 中的值
                            Object value = readMethod.invoke(source);
                            ReflectionUtils.makeAccessible(writeMethod);
                            // 反射写到 target
                            writeMethod.invoke(target, value);
                        } catch (Throwable ex) {
                            throw new FatalBeanException(
                                "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
    }
}

org.springframework.beans.BeanUtils.getPropertyDescriptors org.springframework.beans.CachedIntrospectionResults.forClass

static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
    // 判断缓存中是否存在 存在则返回
    CachedIntrospectionResults results = strongClassCache.get(beanClass);
    if (results != null) {
        return results;
    }
    results = softClassCache.get(beanClass);
    if (results != null) {
        return results;
    }

    // 封装缓存的对象 核心代码在 CachedIntrospectionResults 的构造方法
    results = new CachedIntrospectionResults(beanClass);
    ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;

    if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
        isClassLoaderAccepted(beanClass.getClassLoader())) {
        classCacheToUse = strongClassCache;
    } else {
        if (logger.isDebugEnabled()) {
            logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
        }
        classCacheToUse = softClassCache;
    }

    CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
    return (existing != null ? existing : results);
}

org.springframework.beans.CachedIntrospectionResults.CachedIntrospectionResults

    private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
    try {
        if (logger.isTraceEnabled()) {
            logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
        }
        // 获取bean 的信息  最终调用了:Introspector.getBeanInfo
        this.beanInfo = getBeanInfo(beanClass);

        if (logger.isTraceEnabled()) {
            logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
        }
        this.propertyDescriptors = new LinkedHashMap<>();

        Set<String> readMethodNames = new HashSet<>();

        // This call is slow so we do it once.
        // 获取所有的 PropertyDescriptor
        PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
        for (PropertyDescriptor pd : pds) {
            // 忽略各种不需要的属性
            if (Class.class == beanClass && !("name".equals(pd.getName()) ||
                (pd.getName().endsWith("Name") && String.class == pd.getPropertyType()))) {
                // Only allow all name variants of Class properties
                continue;
            }
            if (URL.class == beanClass && "content".equals(pd.getName())) {
                // Only allow URL attribute introspection, not content resolution
                continue;
            }
            if (pd.getWriteMethod() == null && isInvalidReadOnlyPropertyType(pd.getPropertyType(), beanClass)) {
                // Ignore read-only properties such as ClassLoader - no need to bind to those
                continue;
            }
            if (logger.isTraceEnabled()) {
                logger.trace("Found bean property '" + pd.getName() + "'" +
                    (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
                    (pd.getPropertyEditorClass() != null ?
                        "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
            }
            pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
            // 生成一个 PropertyDescriptor 的map 提高调用速度
            this.propertyDescriptors.put(pd.getName(), pd);
            Method readMethod = pd.getReadMethod();
            if (readMethod != null) {
                readMethodNames.add(readMethod.getName());
            }
        }

        // Explicitly check implemented interfaces for setter/getter methods as well,
        // in particular for Java 8 default methods...
        Class<?> currClass = beanClass;
        while (currClass != null && currClass != Object.class) {
            introspectInterfaces(beanClass, currClass, readMethodNames);
            currClass = currClass.getSuperclass();
        }

        // Check for record-style accessors without prefix: e.g. "lastName()"
        // - accessor method directly referring to instance field of same name
        // - same convention for component accessors of Java 15 record classes
        introspectPlainAccessors(beanClass, readMethodNames);
    } catch (IntrospectionException ex) {
        throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
    }
}

Spring 源码就这么简单,总结下来就是 解析成PropertyDescriptor -> 缓存 -> 循环 target 的 method 去反射调用。

为什么Spring BeanUtils 速度比 Apache BeanUtils 快?

2个源码我们都看完了,实际上2个源码几乎是一模一样的, Apache BeanUtils 慢的原因如主要是:对象拷贝加了各种花里胡哨的校验。

是不是答案特别的神奇,不看源码我也没法下这个结论。

总结

我们已经学习了 Apache BeanUtils 和 Spring BeanUtils 的源码,虽然 Spring BeanUtils 的性能比 Apache BeanUtils 好很多,但是和原生的还是有一定差距。

我们有办法 进一步 的优化 BeanUtils 的性能呢?

还有不管是 Spring BeanUtils 还是 Apache BeanUtils 都只能拷贝相同的属性,如果属性名不一样怎么办?

下一节我就带大家来解决这个2个问题。

写在最后

给大家推荐一个非常完整的Java项目搭建的最佳实践,也是本文的源码出处,由大厂程序员&EasyExcel作者维护。
github地址:https://github.com/zhuangjiaju/easytools
gitee地址:https://gitee.com/zhuangjiaju/easytools

相关推荐

go语言也可以做gui,go-fltk让你做出c++级别的桌面应用

大家都知道go语言生态并没有什么好的gui开发框架,“能用”的一个手就能数的清,好用的就更是少之又少。今天为大家推荐一个go的gui库go-fltk。它是通过cgo调用了c++的fltk库,性能非常高...

旧电脑的首选系统:TinyCore!体积小+精简+速度极快,你敢安装吗

这几天老毛桃整理了几个微型Linux发行版,准备分享给大家。要知道可供我们日常使用的Linux发行版有很多,但其中的一些发行版经常会被大家忽视。其实这些微型Linux发行版是一种非常强大的创新:在一台...

codeblocks和VS2019下的fltk使用中文

在fltk中用中文有点问题。英文是这样。中文就成这个样子了。我查了查资料,说用UTF-8编码就行了。edit->Fileencoding->UTF-8然后保存文件。看下下边的编码指示确...

FLTK(Fast Light Toolkit)一个轻量级的跨平台Python GUI库

FLTK(FastLightToolkit)是一个轻量级的跨平台GUI库,特别适用于开发需要快速、高效且简单界面的应用程序。本文将介绍Python中的FLTK库,包括其特性、应用场景以及如何通过代...

中科院开源 RISC-V 处理器“香山”流片,已成功运行 Linux

IT之家1月29日消息,去年6月份,中科院大学教授、中科院计算所研究员包云岗,发布了开源高性能RISC-V处理器核心——香山。近日,包云岗在社交平台晒出图片,香山芯片已流片,回片后...

Linux 5.13内核有望合并对苹果M1处理器支持的初步代码

预计Linux5.13将初步支持苹果SiliconM1处理器,不过完整的支持工作可能还需要几年时间才能完全完成。虽然Linux已经可以在苹果SiliconM1上运行,但这需要通过一系列的补丁才能...

Ubuntu系统下COM口测试教程(ubuntu port)

1、在待测试的板上下载minicom,下载minicom有两种方法:方法一:在Ubuntu软件中心里面搜索下载方法二:按“Ctrl+Alt+T”打开终端,打开终端后输入“sudosu”回车;在下...

湖北嵌入式软件工程师培训怎么选,让自己脱颖而出

很多年轻人毕业即失业、面试总是不如意、薪酬不满意、在家躺平。“就业难”该如何应对,参加培训是否能改变自己的职业走向,在湖北,有哪些嵌入式软件工程师培训怎么选值得推荐?粤嵌科技在嵌入式培训领域有十几年经...

新阁上位机开发---10年工程师的Modbus总结

前言我算了一下,今年是我跟Modbus相识的第10年,从最开始的简单应用到协议了解,从协议开发到协议讲解,这个陪伴了10年的协议,它一直没变,变的只是我对它的理解和认识。我一直认为Modbus协议的存...

创建你的第一个可运行的嵌入式Linux系统-5

@ZHangZMo在MicrochipBuildroot中配置QT5选择Graphic配置文件增加QT5的配置修改根文件系统支持QT5修改output/target/etc/profile配置文件...

如何在Linux下给zigbee CC2530实现上位机

0、前言网友提问如下:粉丝提问项目框架汇总下这个网友的问题,其实就是实现一个网关程序,内容分为几块:下位机,通过串口与上位机相连;下位机要能够接收上位机下发的命令,并解析这些命令;下位机能够根据这些命...

Python实现串口助手 - 03串口功能实现

 串口调试助手是最核心的当然是串口数据收发与显示的功能,pzh-py-com借助的是pySerial库实现串口收发功能,今天痞子衡为大家介绍pySerial是如何在pzh-py-com发挥功能的。一、...

为什么选择UART(串口)作为调试接口,而不是I2C、SPI等其他接口

UART(通用异步收发传输器)通常被选作调试接口有以下几个原因:简单性:协议简单:UART的协议非常简单,只需设置波特率、数据位、停止位和校验位就可以进行通信。相比之下,I2C和SPI需要处理更多的通...

同一个类,不同代码,Qt 串口类QSerialPort 与各种外设通讯处理

串口通讯在各种外设通讯中是常见接口,因为各种嵌入式CPU中串口标配,工业控制中如果不够还通过各种串口芯片进行扩展。比如spi接口的W25Q128FV.对于软件而言,因为驱动接口固定,软件也相对好写,因...

嵌入式linux为什么可以通过PC上的串口去执行命令?

1、uboot(负责初始化基本硬bai件,如串口,网卡,usb口等,然du后引导系统zhi运行)2、linux系统(真正的操作系统)3、你的应用程序(基于操作系统的软件应用)当你开发板上电时,u...

取消回复欢迎 发表评论: