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 对象:

/**
 * 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);
}

输出结果:

     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,非常的通俗易懂,看懂了然后再看源代码就非常容易了。

复制的代码一模一样:

/**
 * 自己写一个简单的 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: 方法,用这个对象可以反射掉调用

 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

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

 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

  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

 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

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

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

  • 22
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

庄家钜Java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值