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

👆🏻👆🏻👆🏻关注博主,让你的代码变得更加优雅。

前言

Apache BeanUtils 是Java中用来复制2个对象属性的一个类型。

上一篇文章我们讲到了 Apache BeanUtils 性能相对比较差,今天我不仅仅要带你学习源代码 ,更要带你手把手写一个Apache BeanUtils。

最佳实践

直接上案例

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

简单的复制对象

直接上代码:

创建一个java 对象:

java 复制代码
/**
 * Apache BeanUtils 使用的demo
 */
@Test
public void demo() throws Exception {
    BeanUtilsDemoDTO beanUtilsDemo = new BeanUtilsDemoDTO();
    beanUtilsDemo.setId("id");
    beanUtilsDemo.setFirstName("firstName");

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

输出结果:

text 复制代码
     20:21:56.949 [main] INFO com.github.zhuangjiaju.easytools.test.demo.beanutils.ApacheBeanUtilsTest -- 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 的值。

直接自己写一个简单的 BeanUtils

源码有点复杂,我先直接写一个简单的 BeanUtils,非常的通俗易懂,看懂了然后再看源代码就非常容易了。

复制的代码一模一样:

java 复制代码
/**
 * 自己写一个简单的 BeanUtils
 */
@Test
public void custom() throws Exception {
    BeanUtilsDemoDTO beanUtilsDemo = new BeanUtilsDemoDTO();
    beanUtilsDemo.setId("id");
    beanUtilsDemo.setFirstName("firstName");

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

我们自己实现的工具类:

前置知识:

Introspector.getBeanInfo: 是 Java 自带的一个类,可以获取一个类的 BeanInfo 信息,然后获取属性的描述资料 PropertyDescriptor

BeanInfo : bean 的描述信息

PropertyDescriptor: bean 的属性的资料信息 ,可以获取到属性的 get/set 方法

Method: 方法,用这个对象可以反射掉调用

java 复制代码
 public static class MyBeanUtils {

    /**
     * 复制方法
     *
     * @param dest
     * @param orig
     * @throws Exception
     */
    public static void copyProperties(Object dest, Object orig) throws Exception {
        // 获取目标对象的 PropertyDescriptor 属性资料
        PropertyDescriptor[] destPropertyDescriptors = Introspector.getBeanInfo(dest.getClass(), Object.class)
            .getPropertyDescriptors();
        // 获取来源对象的 PropertyDescriptor 属性资料
        PropertyDescriptor[] origPropertyDescriptors = Introspector.getBeanInfo(orig.getClass(), Object.class)
            .getPropertyDescriptors();
        // 上面2个 在 Apache BeanUtils 还加了缓存

        // 循环目标对象
        for (PropertyDescriptor propertyDescriptor : destPropertyDescriptors) {
            // 获取属性名
            String name = propertyDescriptor.getName();
            // 循环来源对象的属性名
            for (PropertyDescriptor origPropertyDescriptor : origPropertyDescriptors) {
                // 2个属性名匹配上了
                if (name.equals(origPropertyDescriptor.getName())) {
                    // 直接获取 method 然后反色调用即可 就设置了数据
                    propertyDescriptor.getWriteMethod().invoke(dest,
                        origPropertyDescriptor.getReadMethod().invoke(orig));
                    break;
                }
            }
        }
    }
}

代码是不是非常的容易?就是循环目标对象的属性,然后循环来源对象的属性,然后匹配上了就反射调用即可。

和 Apache BeanUtils 的源码逻辑基本一样,只是没有加缓存之类的。

源码解析

org.apache.commons.beanutils.BeanUtils.copyProperties

java 复制代码
public static void copyProperties(final Object dest, final Object orig)
    throws IllegalAccessException, InvocationTargetException {

    // BeanUtilsBean 放在了 ThreadLocal 里面,所以是不可以并发的,但是通过 ThreadLocal 保障了BeanUtilsBean不会并发 也不会每次都new 
    // 直接调用 copyProperties
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

org.apache.commons.beanutils.BeanUtilsBean.copyProperties

java 复制代码
 public void copyProperties(final Object dest, final Object orig)
    throws IllegalAccessException, InvocationTargetException {

    // Validate existence of the specified beans
    if (dest == null) {
        throw new IllegalArgumentException
            ("No destination bean specified");
    }
    if (orig == null) {
        throw new IllegalArgumentException("No origin bean specified");
    }
    if (log.isDebugEnabled()) {
        log.debug("BeanUtils.copyProperties(" + dest + ", " +
            orig + ")");
    }

    // Copy the properties, converting as necessary
    if (orig instanceof DynaBean) {
        final DynaProperty[] origDescriptors =
            ((DynaBean)orig).getDynaClass().getDynaProperties();
        for (DynaProperty origDescriptor : origDescriptors) {
            final String name = origDescriptor.getName();
            // Need to check isReadable() for WrapDynaBean
            // (see Jira issue# BEANUTILS-61)
            if (getPropertyUtils().isReadable(orig, name) &&
                getPropertyUtils().isWriteable(dest, name)) {
                final Object value = ((DynaBean)orig).get(name);
                copyProperty(dest, name, value);
            }
        }
    } else if (orig instanceof Map) {
        @SuppressWarnings("unchecked")
        final
        // Map properties are always of type <String, Object>
        Map<String, Object> propMap = (Map<String, Object>)orig;
        for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
            final String name = entry.getKey();
            if (getPropertyUtils().isWriteable(dest, name)) {
                copyProperty(dest, name, entry.getValue());
            }
        }
    } else /* if (orig is a standard JavaBean) */ {
        // 这里比较核心 获取来源的PropertyDescriptor 属性资料 和我们自己实现的代码 一样
        // getPropertyDescriptors 我们会继续跟进
        final PropertyDescriptor[] origDescriptors =
            getPropertyUtils().getPropertyDescriptors(orig);
        // 循环来源的 属性资料
        for (PropertyDescriptor origDescriptor : origDescriptors) {
            final String name = origDescriptor.getName();
            if ("class".equals(name)) {
                continue; // No point in trying to set an object's class
            }
            if (getPropertyUtils().isReadable(orig, name) &&
                getPropertyUtils().isWriteable(dest, name)) {
                try {
                    final Object value =
                        getPropertyUtils().getSimpleProperty(orig, name);
                    // 调用复制参数 
                    // copyProperty 我们会继续跟进
                    copyProperty(dest, name, value);
                } catch (final NoSuchMethodException e) {
                    // Should not happen
                }
            }
        }
    }

}

org.apache.commons.beanutils.PropertyUtilsBean.getPropertyDescriptors(java.lang.Object)

org.apache.commons.beanutils.PropertyUtilsBean.getPropertyDescriptors(java.lang.Class<?>)

org.apache.commons.beanutils.PropertyUtilsBean.getIntrospectionData

java 复制代码
  private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) {
    if (beanClass == null) {
        throw new IllegalArgumentException("No bean class specified");
    }

    // Look up any cached information for this bean class
    // 和我们自己写的比,这里核心是加了一个descriptorsCache 的缓存 
    BeanIntrospectionData data = descriptorsCache.get(beanClass);
    if (data == null) {
        data = fetchIntrospectionData(beanClass);
        descriptorsCache.put(beanClass, data);
    }

    return data;
}

org.apache.commons.beanutils.PropertyUtilsBean.fetchIntrospectionData

org.apache.commons.beanutils.DefaultBeanIntrospector.introspect

java 复制代码
 public void introspect(final IntrospectionContext icontext) {
    BeanInfo beanInfo = null;
    try {
        // 这里和我们自己实现的一样 可以获取一个类的 BeanInfo 信息
        beanInfo = Introspector.getBeanInfo(icontext.getTargetClass());
    } catch (final IntrospectionException e) {
        // no descriptors are added to the context
        log.error(
            "Error when inspecting class " + icontext.getTargetClass(),
            e);
        return;
    }

    //  获取 bean 的 PropertyDescriptor 属性的资料信息
    PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
    if (descriptors == null) {
        descriptors = new PropertyDescriptor[0];
    }

    handleIndexedPropertyDescriptors(icontext.getTargetClass(),
        descriptors);
    icontext.addPropertyDescriptors(descriptors);
}

通过以上方法,我们就拿到了 PropertyDescriptor[] origDescriptors 接下来我们看 copyProperty

org.apache.commons.beanutils.BeanUtilsBean.copyProperty

java 复制代码
public void copyProperty(final Object bean, String name, Object value)
    throws IllegalAccessException, InvocationTargetException {

    // Trace logging (if enabled)
    if (log.isTraceEnabled()) {
        final StringBuilder sb = new StringBuilder("  copyProperty(");
        sb.append(bean);
        sb.append(", ");
        sb.append(name);
        sb.append(", ");
        if (value == null) {
            sb.append("<NULL>");
        } else if (value instanceof String) {
            sb.append((String)value);
        } else if (value instanceof String[]) {
            final String[] values = (String[])value;
            sb.append('[');
            for (int i = 0; i < values.length; i++) {
                if (i > 0) {
                    sb.append(',');
                }
                sb.append(values[i]);
            }
            sb.append(']');
        } else {
            sb.append(value.toString());
        }
        sb.append(')');
        log.trace(sb.toString());
    }

    // Resolve any nested expression to get the actual target bean
    Object target = bean;
    final Resolver resolver = getPropertyUtils().getResolver();
    while (resolver.hasNested(name)) {
        try {
            target = getPropertyUtils().getProperty(target, resolver.next(name));
            name = resolver.remove(name);
        } catch (final NoSuchMethodException e) {
            return; // Skip this property setter
        }
    }
    if (log.isTraceEnabled()) {
        log.trace("    Target bean = " + target);
        log.trace("    Target name = " + name);
    }

    // Declare local variables we will require
    final String propName = resolver.getProperty(name); // Simple name of target property
    Class<?> type = null;                         // Java type of target property
    final int index = resolver.getIndex(name);         // Indexed subscript value (if any)
    final String key = resolver.getKey(name);           // Mapped key value (if any)

    // Calculate the target property type
    if (target instanceof DynaBean) {
        final DynaClass dynaClass = ((DynaBean)target).getDynaClass();
        final DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
        if (dynaProperty == null) {
            return; // Skip this property setter
        }
        type = dynaPropertyType(dynaProperty, value);
    } else {
        PropertyDescriptor descriptor = null;
        try {
            descriptor =
                getPropertyUtils().getPropertyDescriptor(target, name);
            if (descriptor == null) {
                return; // Skip this property setter
            }
        } catch (final NoSuchMethodException e) {
            return; // Skip this property setter
        }
        type = descriptor.getPropertyType();
        if (type == null) {
            // Most likely an indexed setter on a POJB only
            if (log.isTraceEnabled()) {
                log.trace("    target type for property '" +
                    propName + "' is null, so skipping ths setter");
            }
            return;
        }
    }
    if (log.isTraceEnabled()) {
        log.trace("    target propName=" + propName + ", type=" +
            type + ", index=" + index + ", key=" + key);
    }

    // Convert the specified value to the required type and store it
    if (index >= 0) {                    // Destination must be indexed
        value = convertForCopy(value, type.getComponentType());
        try {
            getPropertyUtils().setIndexedProperty(target, propName,
                index, value);
        } catch (final NoSuchMethodException e) {
            throw new InvocationTargetException
                (e, "Cannot set " + propName);
        }
    } else if (key != null) {            // Destination must be mapped
        // Maps do not know what the preferred data type is,
        // so perform no conversions at all
        // FIXME - should we create or support a TypedMap?
        try {
            getPropertyUtils().setMappedProperty(target, propName,
                key, value);
        } catch (final NoSuchMethodException e) {
            throw new InvocationTargetException
                (e, "Cannot set " + propName);
        }
    } else {                             // Destination must be simple
        value = convertForCopy(value, type);
        try {
            // 核心我们看这里 设置属性值
            getPropertyUtils().setSimpleProperty(target, propName, value);
        } catch (final NoSuchMethodException e) {
            throw new InvocationTargetException
                (e, "Cannot set " + propName);
        }
    }

}

org.apache.commons.beanutils.PropertyUtilsBean.setSimpleProperty

java 复制代码
public void setSimpleProperty(final Object bean,
    final String name, final Object value)
    throws IllegalAccessException, InvocationTargetException,
    NoSuchMethodException {

    if (bean == null) {
        throw new IllegalArgumentException("No bean specified");
    }
    if (name == null) {
        throw new IllegalArgumentException("No name specified for bean class '" +
            bean.getClass() + "'");
    }

    // Validate the syntax of the property name
    if (resolver.hasNested(name)) {
        throw new IllegalArgumentException
            ("Nested property names are not allowed: Property '" +
                name + "' on bean class '" + bean.getClass() + "'");
    } else if (resolver.isIndexed(name)) {
        throw new IllegalArgumentException
            ("Indexed property names are not allowed: Property '" +
                name + "' on bean class '" + bean.getClass() + "'");
    } else if (resolver.isMapped(name)) {
        throw new IllegalArgumentException
            ("Mapped property names are not allowed: Property '" +
                name + "' on bean class '" + bean.getClass() + "'");
    }

    // Handle DynaBean instances specially
    if (bean instanceof DynaBean) {
        final DynaProperty descriptor =
            ((DynaBean)bean).getDynaClass().getDynaProperty(name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                name + "' on dynaclass '" +
                ((DynaBean)bean).getDynaClass() + "'");
        }
        ((DynaBean)bean).set(name, value);
        return;
    }

    // Retrieve the property setter method for the specified property
    final PropertyDescriptor descriptor =
        getPropertyDescriptor(bean, name);
    if (descriptor == null) {
        throw new NoSuchMethodException("Unknown property '" +
            name + "' on class '" + bean.getClass() + "'");
    }
    // 通过 PropertyDescriptor 获取道理 set 方法 的 Method
    final Method writeMethod = getWriteMethod(bean.getClass(), descriptor);
    if (writeMethod == null) {
        throw new NoSuchMethodException("Property '" + name +
            "' has no setter method in class '" + bean.getClass() + "'");
    }

    // Call the property setter method
    final Object[] values = new Object[1];
    values[0] = value;
    if (log.isTraceEnabled()) {
        final String valueClassName =
            value == null ? "<null>" : value.getClass().getName();
        log.trace("setSimpleProperty: Invoking method " + writeMethod
            + " with value " + value + " (class " + valueClassName + ")");
    }
    // 这个方法就是 简单的 writeMethod 调用 invoke 方法 这样子我们的值就设置好了
    invokeMethod(writeMethod, bean, values);

好了,这样子一个值就复制到新的对象里面了,是不是很简单?

总结

今天学习了 Apache BeanUtils 的源码,总体上就是一个缓存+反射的调用,看是记不住的,大家赶快打开自己的电脑跟几遍源码吧。

后面还会带大家看 Spring BeanUtils 的源码,欢迎持续关注。

写在最后

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

相关推荐
刃神太酷啦11 分钟前
力扣校招算法通关:双指针技巧全场景拆解 —— 从数组操作到环检测的高效解题范式
java·c语言·数据结构·c++·算法·leetcode·职场和发展
Mos_x23 分钟前
计算机组成原理核心知识点梳理
java·后端
墨寒博客栈27 分钟前
Linux基础常用命令
java·linux·运维·服务器·前端
回忆是昨天里的海32 分钟前
k8s-部署springboot容器化应用
java·容器·kubernetes
INFINI Labs43 分钟前
使用 Docker Compose 轻松实现 INFINI Console 离线部署与持久化管理
java·docker·eureka·devops·docker compose·console·easyserach
Cosolar44 分钟前
国产麒麟系统 aarch64 架构 PostgreSQL 15 源码编译安装完整教程
java·后端
GalaxyPokemon1 小时前
PlayerFeedback 插件开发日志
java·服务器·前端
天天摸鱼的java工程师1 小时前
别再写那些重复代码了!8年Java老兵教你用 Hutool 提升开发效率
java·后端