重复造轮子系列之BeanUtils.copyProperties()的实现

前言

相信大家在开发中一定都用过Spring框架提供的一个小工具类:BeanUtils,特别是其中的copyProperties()方法,它可以将一个对象中的属性复制到另一个对象中,不过复制的前提是两个对象中的属性的名称和类型要一样,在开发中大量使用了这个工具类,所以想着探究一下实现原理,要是你说你没用过,那我也不知道说啥,不管别人信不信,反正我是不信。

反射

可能你会说,Java里边有一套很厉害的API,我们称其为反射API,它可以在运行时获取任意对象的任意属性和方法,进而实现对属性的更改和方法的调用,如果利用反射来实现属性的复制的话,却略显麻烦,我们需要获得源对象的getter方法,通过反射去调用,获得其值,然后再获取目标对象的setter方法,同样通过反射去调用,设置相应的值。不过我们也可以绕过调用getter和setter方法,直接对属性进行赋值,但是bean中的属性一般都是私有的,我们需要解除私有限定,这样利用反射直接对私有属性进行修改是非常不提倡的,也破坏了类的封装性。如果非要利用反射机制实现属性的复制,可能会写出如下的典型代码:

  public class ReflectUtils {

   public static void main(String[] args)throws Exception{
       Student soureStu = new Student();
       soureStu.setAge("1223");
       soureStu.setName("哈哈哈");
       Student targetStu = new Student();

       Class sourceClas = soureStu.getClass();
       Class targetClas = targetStu.getClass();

       //获取源对象的所有属性
       Field[] sourceFields = sourceClas.getDeclaredFields();
       System.out.println("targetStu复制前"+targetStu);
       for(Field field : sourceFields){
           if(!"class".equals(field.getName())){
               //获取源对象的属性值
               field.setAccessible(true);
               Object value =  field.get(soureStu);
               //暴力获取对应的属性
               Field targetField = targetClas.getDeclaredField(field.getName());
               targetField.setAccessible(true);
              //暴力设置对应的属性
               targetField.set(targetStu,value);
           }
       }
       System.out.println("targetStu复制后"+targetStu);
   }
}

运行结果是:

targetStu复制前Student{name='我是默认的name', age='我是默认的age'}
targetStu复制后Student{name='哈哈哈', age='1223'}

在这段代码中,没有通过调用暴露的setter和getter方法去改变和获取相应的值,而是通过反射暴力解除了私有限定,并对私有属性进行了一些操作,这破坏了类的封装性,是开发中是非常不提倡的,可能会被揍哦。

内省机制

Java也提供了一套专门用于获取对象getter和setter方法的API,这些API位于java.beans包下,通过这些API,我们可以非常轻松地实现对getter和setter方法的获取,进而实现对其的调用,这也是Spring框架实现BeanUtils的核心。

下面我们直接上代码,实现一个自己的BeanUtils.copyProperties()方法。

步骤一: 定义一个工具类My_BeanUtils,里面有一个静态方法copyProperties(),该方法接收两个类型为Object的形参source和target,该方法的签名即为:

  public static void copyProperties(Object source, Object target);

步骤二: 获取target对象的对象信息,可以通过Introspector中的静态方法getBeanInfo()来获取,该方法的返回值是一个接口BeanInfo,具体对象的信息由其实现类来提供。

public static void copyProperties (Object source, Object target){
        BeanInfo targetBeanInfo = null;
        try{
            //获取目标对象的对象信息
            targetBeanInfo = Introspector.getBeanInfo(target.getClass());
        }catch (Exception e){
            e.printStackTrace();
        }
  }

那么获取到BeanInfo接口有什么用处呢?在下一步中我们可以通过获取到的BeanInfo接口获得对象的属性描述器,我们可以暂时把属性描述器理解为属性的getter和setter方法。

步骤三: 获取源对象的BeanInfo,并从BeanInfo中获取源对象所有的属性描述器,并遍历。

public static void copyProperties (Object source, Object target){
        BeanInfo targetBeanInfo = null;
        try{
            //获取目标对象的对象信息
            targetBeanInfo = Introspector.getBeanInfo(target.getClass());
        }catch (Exception e){
            e.printStackTrace();
        }
        try{
            //获取源对象的对象信息
            BeanInfo beanInfo = Introspector.getBeanInfo(source.getClass());
            //从对象信息中获取所有属性的属性描述器
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
            //遍历源对象的属性描述器
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors){
                //忽略存在的class属性
                if(!"class".equals(propertyDescriptor.getName())){

                    //获得source对象中属性的get方法
                    Method readMethod = propertyDescriptor.getReadMethod();
                    //获取source对象中的属性名
                    String key = propertyDescriptor.getName();
                    //通过反射调用get方法,获取source对象中的该属性的值
                    Object oldValue = readMethod.invoke(source);
                    //获取source对象属性的类型
                    String parameterName = propertyDescriptor.getPropertyType().getName();

                    System.out.println("在source对象中查询到属性:"+key);
               }
           }
     }

在这一步,我们已经获取到了源对象的每个属性的名称,类型和值以及其对应的getter和setter方法,在获取值的时候,通过readMethod.invoke()实际上还是利用了反射。我们已经获取到了值,下面就该把值复制到target中了。

步骤四: 在上面我们在一个循环中获取到了source对象的每一个属性名称,值和类型,当然还有其对应的属性描述器,下面我们要获取target对象中同名属性的属性描述器,我们需要使用一个map将target中所有的属性描述器缓存起来,以便获取,map的key是属性的名称,value则为对应属性的属性描述器。

private static Map<String, PropertyDescriptor> createCacheMap (Class bean){
        Map<String, PropertyDescriptor> propertyDescriptorMap = new HashMap<>(10);
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(bean);
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
            for(PropertyDescriptor propertyDescriptor : propertyDescriptors){
                if(!"class".equals(propertyDescriptor.getName())){
                    propertyDescriptorMap.put(propertyDescriptor.getName(),propertyDescriptor);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return propertyDescriptorMap;
    }

当然我们可以将该map作为类的属性,初始化了之后,我们就可以方便地获取target中对应属性的属性描述器。

步骤五: 将source中属性的值设置进target对应的属性中。

  //获取target对象属性的属性描述器,即上面取到的key在目标对象中的属性描述器
                    target_propertyDescriptor = getPropertyDescriptor(target.getClass(),key);
                    if(null != target_propertyDescriptor){
                        //获取target对象属性的set方法
                        Method writeMethod = target_propertyDescriptor.getWriteMethod();
                        //获取target对象属性的类型
                        String name = target_propertyDescriptor.getPropertyType().getName();
                        //数据类型一致,则复制值
                        if(parameterName.equals(name)){
                            //调用set方法向target对象中设置值
                            System.out.println("开始为target设置值,设置的当前属性是"+key);
                            //通过反射调用set方法,设置对应的值
                            writeMethod.invoke(target,oldValue);
                            System.out.println("设置值成功");
                        }else{
                            System.out.println("源对象属性"+key+"的类型为"+parameterName+",目标对象属性类型为"+name+",类型不一致,将跳过该属性");
                        }
                    }else{
                        System.out.println("在target中没有查询到"+key+"的属性");
                    }
 

在这一步中,我们将获取到的source中的key作为参数去获取target中该参数对应的属性描述器,若能获取到,则说明在target对象中也存在该属性,进而再去实现调用target的getMethod,设置对应的值。 值得注意的是:我在代码中增加了同名属性类型的判断,若类型不一致,则给出相应的提示,在Spring中是没有这样的提示的,默认会跳过同名但是不同类型的属性。

总结

整个实现过程差不多就是这样,来总结一下:

  1. 获取对象的对象信息BeanInfo,通过Introspector.getBeanInfo(Class class);来获取。
  2. 通过BeanInfo获取对象的属性描述器,通过PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();来获取。
  3. 通过源对象的属性描述器去获取该属性的名称,值和类型,当然获取值这一步需要通过反射来完成。
  4. 根据上一步获取到的属性的名称去target对象中获取对应的属性描述器,若是获取成功,则进行类型的判断,若一致,则可以通过调用target的writerMethod来实现值的设置。
  5. 需要注意的是,若是类中没有属性,但是存在相应的getter和setter方法,jvm也会默认该属性是存在的。

我已将代码上传至GitHub,你可以点击这里查看相应的代码。

暂无评论
发表新评论