java之通过反射生成并初始化对象

java之通过反射生成并初始化对象

在博文 《java之的读取文件大全》 中读取csv文件后,需要自己将csv文件的对象转为自己的DO对象,那么有没有办法我直接穿进去一个DO的class对象,内部实现生成对象,并利用 CSVRecord 对象对其进行初始化呢 ?

本篇主要是为了解决上面的这个问题,实现了一个非常初级转换方法,然后会分析下大名鼎鼎的BeanUtils是如何实现这种功能的

1. CSVRecord对象转xxxBO对象

在做之前,先把csv的读取相关代码贴出来,具体的实现逻辑详解可以参考 《java之的读取文件大全》

CsvUtil.java

/**
 * 读取文件
 */
public static InputStream getStreamByFileName(String fileName) throws IOException {
        if (fileName == null) {
            throw new IllegalArgumentException("fileName should not be null!");
        }

        if (fileName.startsWith("http")) { // 网络地址
            URL url = new URL(fileName);
            return url.openStream();
        } else if (fileName.startsWith("/")) { // 绝对路径
            Path path = Paths.get(fileName);
            return Files.newInputStream(path);
        } else  { // 相对路径
            return FileUtil.class.getClassLoader().getResourceAsStream(fileName);
        }
    }

/**
* 读取csv文件, 返回结构话的对象
* @param filename csv 路径 + 文件名, 支持绝对路径 + 相对路径 + 网络文件
* @param headers  csv 每列的数据
* @return
* @throws IOException
*/
public static List<CSVRecord> read(String filename, String[] headers) throws IOException {
   try (Reader reader = new InputStreamReader(getStreamByFileName(fileName), Charset.forName("UTF-8"))) {
       CSVParser csvParser = new CSVParser(reader,
               CSVFormat.INFORMIX_UNLOAD_CSV.withHeader(headers)
       );

       return csvParser.getRecords();
   }
}

word.csv 文件

dicId,"name",rootWord,weight
1,"质量",true,0.1
2,"服务",true,0.2
3,"发货",,0.1
4,"性价比",false,0.4
5,"尺码",true,0.4

测试用例

@Getter
@Setter
@ToString
static class WordDO {
   long dicId;

   String name;

   Boolean rootWord;

   Float weight;

   public WordDO() {
   }
}

@Test
public void testCsvRead() throws IOException {
   String fileName = "word.csv";
   List<CSVRecord> list = CsvUtil.read(fileName, new String[]{"dicId", "name", "rootWord", "weight"});
   Assert.assertTrue(list != null && list.size() > 0);

   List<WordDO> words = list.stream()
           .filter(csvRecord -> !"dicId".equals(csvRecord.get("dicId")))
           .map(this::parseDO).collect(Collectors.toList());
   logger.info("the csv words: {}", words);
}


private WordDO parseDO(CSVRecord csvRecord) {
   WordDO wordDO = new WordDO();
   wordDO.dicId = Integer.parseInt(csvRecord.get("dicId"));
   wordDO.name = csvRecord.get("name");
   wordDO.rootWord = Boolean.valueOf(csvRecord.get("rootWord"));
   wordDO.weight = Float.valueOf(csvRecord.get("weight"));
   return wordDO;
}

输出结果

16:17:27.145 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=1, name=质量, rootWord=true, weight=0.1)
16:17:27.153 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=2, name=服务, rootWord=true, weight=0.2)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=3, name=发货, rootWord=false, weight=0.1)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=4, name=性价比, rootWord=false, weight=0.4)
16:17:27.154 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=5, name=尺码, rootWord=true, weight=0.4)

从上面的使用来看,每次都要自己对解析出来的 CsvRecord 进行对象转换, 我们的目标就是把这个集成在 CsvUtil 内部去实现

设计思路

反射创建对象,获取对象的所有属性,然后在属性前面加 set 表示设置属性的方法(boolea类型的属性可能是 isXXX格式), 通过反射设置方法的属性值

  • 创建对象: T obj = clz.newInstance();
  • 获取所有属性: Field[] fields = clz.getDeclaredFields();
  • 设置属性值
    • 方法名: fieldSetMethodName = "set" + upperCase(field.getName());
    • 属性值,需要转换对应的类型: fieldValue = this.parseType(value, field.getType());
    • 获取设置属性方法 : Method method = clz.getDeclaredMethod(fieldSetMethodName, field.getType());
    • 设置属性: method.invoke(obj, fieldValue);

实现代码

基本结构如上,先贴出实现的代码,并对其中的几点做一下简短的说明

private <T> T parseBO(CSVRecord csvRecord, Class<T> clz) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        // 创建BO对象
        T obj = clz.newInstance();


        // 获取声明的所有成员变量
        Field[] fields = clz.getDeclaredFields();


        // 保存属性对应的csvRecord中的值
        String value;
        String fieldSetMethodName;
        Object fieldValue;
        for (Field field : fields) {
            // 设置为可访问
            field.setAccessible(true);

            // 将value转换为目标类型
            value = csvRecord.get(field.getName());
            if (value == null) {
                continue;
            }
            fieldValue = this.parseType(value, field.getType());


            // 获取属性对应的设置方法名
            fieldSetMethodName = "set" + upperCase(field.getName());
            Method method = clz.getDeclaredMethod(fieldSetMethodName, field.getType());
            

            // 设置属性值
            method.invoke(obj, fieldValue);
        }


        return obj;
    }


    // 首字母变大写
    private String upperCase(String str) {
        char[] ch = str.toCharArray();
//      也可以直接用下面的记性转大写
//      ch[0] = Character.toUpperCase(ch[0]);
        if (ch[0] >= 'a' && ch[0] <= 'z') {
            ch[0] = (char) (ch[0] - 32);
        }
        return new String(ch);
    }


    /**
     * 类型转换
     * 
     * @param value 原始数据格式
     * @param type 期待转换的类型
     * @return 转换后的数据对象
     */
    private Object parseType(String value, Class type) {

        if (type == String.class) {
            return value;
        } else if (type == int.class) {
            return value == null ? 0 : Integer.parseInt(value);
        } else if (type == float.class) {
            return value == null ? 0f : Float.parseFloat(value);
        } else if (type == long.class) {
            return value == null ? 0L : Long.parseLong(value);
        } else if (type == double.class) {
            return value == null ? 0D : Double.parseDouble(value);
        } else if (type == boolean.class) {
            return value != null && Boolean.parseBoolean(value);
        } else if (type == byte.class) {
            return value == null || value.length() == 0 ? 0 : value.getBytes()[0];
        } else if (type == char.class) {
            if (value == null || value.length() == 0) {
                return 0;
            }

            char[] chars = new char[1];
            value.getChars(0, 1, chars, 0);
            return chars[0];
        }

        // 非基本类型,
        if (StringUtils.isEmpty(value)) {
            return null;
        }


        if (type == Integer.class) {
            return Integer.valueOf(value);
        } else if (type == Long.class) {
            return Long.valueOf(value);
        } else if (type == Float.class) {
            return Float.valueOf(value);
        } else if (type == Double.class) {
            return Double.valueOf(value);
        } else if (type == Boolean.class) {
            return Boolean.valueOf(value);
        } else if (type == Byte.class) {
            return value.getBytes()[0];
        } else if (type == Character.class) {
            char[] chars = new char[1];
            value.getChars(0, 1, chars, 0);
            return chars[0];
        }


        throw new IllegalStateException("argument not basic type! now type:" + type.getName());
    }
1. 字符串的首字母大写

最直观的做法是直接用String的内置方法

return str.substring(0,1).toUpperCase() + str.substring(1);

因为substring内部实际上会新生成一个String对象,所以上面这行代码实际上新生成了三个对象(+号又生成了一个),而我们的代码中, 则直接获取String对象的字符数组,修改后重新生成一个String返回,实际只新生成了一个对象,稍微好一点

2. string 转基本数据类型

注意一下将String转换为基本的数据对象,封装对象时, 需要对空的情况进行特殊处理

3. 几个限制

BO对象必须是可实例化的

举一个反例, 下面的这个 WordBO对象就没办法通过反射创建对象

public class CsvUtilTest {
    @Getter
    @Setter
    @ToString
    private static class WordBO {
        long dicId;

        String name;

        Boolean rootWord;

        Float weight;

//        public WordDO() {
//        }
    }
}

解决办法是加一个默认的无参构造方法即可


BO对象要求

  • 显示声明无参构造方法
  • 属性 abc 的设置方法命名为 setAbc(xxx)
  • 属性都是基本的数据结构 (若对象是以json字符串格式存csv文件时,可利用json工具进行反序列化,这样可能会更加简单)
  • BO对象的属性名与CsvRecord中的对象名相同

测试一发

@Test
public void testCsvReadV2() throws IOException {
   String fileName = "word.csv";
   List<CSVRecord> list = CsvUtil.read(fileName, new String[]{"dicId", "name", "rootWord", "weight"});
   Assert.assertTrue(list != null && list.size() > 0);
   
   try {
       List<WordDO> words = new ArrayList<>(list.size() - 1);
       for (int i = 1; i < list.size(); i++) {
           words.add(parseDO(list.get(i), WordDO.class));
       }

       words.stream().forEach(
               word -> logger.info("the csv words: {}", word)
       );
   } catch (Exception e) {
       logger.error("parse DO error! e: {}", e);
   }
}

输出结果

17:17:14.640 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=1, name=质量, rootWord=true, weight=0.1)
17:17:14.658 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=2, name=服务, rootWord=true, weight=0.2)
17:17:14.658 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=3, name=发货, rootWord=null, weight=0.1)
17:17:14.659 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=4, name=性价比, rootWord=false, weight=0.4)
17:17:14.659 [main] INFO  c.h.h.q.file.test.FileUtilTest - the csv words: CsvUtilTest.WordDO(dicId=5, name=尺码, rootWord=true, weight=0.4)

注意这里发货这一个输出的 rootWord为null, 而上面的是输出false, 主要是因为解析逻辑不同导致


2. BeanUtils 分析

顶顶大名的BeanUtils, 目前流行的就有好多个 Apache的两个版本:(反射机制) org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig) org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig) Spring版本:(反射机制) org.springframework.beans.BeanUtils.copyProperties(Object source, Object target, Class editable, String[] ignoreProperties) cglib版本:(使用动态代理,效率高) net.sf.cglib.beans.BeanCopier.copy(Object paramObject1, Object paramObject2, Converter paramConverter)

本篇分析的目标放在 BeanUtils.copyProperties

先看一个使用的case

DoA.java

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoA {

    private String name;

    private long phone;
}

DoB.java

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DoB {
    private String name;

    private long phone;
}

测试case

@Test
public void testBeanCopy() throws InvocationTargetException, IllegalAccessException {
   DoA doA = new DoA();
   doA.setName("yihui");
   doA.setPhone(1234234L);

   DoB doB = new DoB();
   BeanUtils.copyProperties(doB, doA);
   log.info("doB: {}", doB);

   BeanUtils.setProperty(doB, "name", doA.getName());
   BeanUtils.setProperty(doB, "phone", doB.getPhone());
   log.info("doB: {}", doB);
}

1, 属性拷贝逻辑

实际看下属性拷贝的代码,

  • 获取对象的属性描述类 PropertyDescriptor,
  • 然后遍历可以进行赋值的属性 getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)
  • 获取orgi属性名 + 属性值,执行赋值 copyProperty(dest, name, value);
PropertyDescriptor[] origDescriptors =
      getPropertyUtils().getPropertyDescriptors(orig);
for (int i = 0; i < origDescriptors.length; i++) {
 String name = origDescriptors[i].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 {
         Object value =
             getPropertyUtils().getSimpleProperty(orig, name);
        // 获取源对象的 属性名 + 属性值, 调用 copyProperty方法实现赋值
         copyProperty(dest, name, value);
     } catch (NoSuchMethodException e) {
         // Should not happen
     }
 }

2. PropertyDescriptor

jdk说明: A PropertyDescriptor describes one property that a Java Bean exports via a pair of accessor methods.

根据class得到这个属性之后,基本上就get到各种属性,以及属性的设置方法了

内部的几个关键属性

// bean 的成员类型
private Reference<? extends Class<?>> propertyTypeRef;
// bean 的成员读方法
private final MethodRef readMethodRef = new MethodRef();
// bean 的成员写方法
private final MethodRef writeMethodRef = new MethodRef();

MethodRef.java, 包含了方法的引用

final class MethodRef {
    // 方法签名 , 如 : public void com.hust.hui.quicksilver.file.test.dos.DoA.setName(java.lang.String)
    private String signature;
    private SoftReference<Method> methodRef;
    // 方法所在的类对应的class
    private WeakReference<Class<?>> typeRef;
}

一个实例的截图如下

如何获取 PropertyDescriptor 对象呢 ? 通过 java.beans.BeanInfo#getPropertyDescriptors 即可, 顺着 PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig); , 一路摸到如何根据 class 获取 BeanInfo对象, 贴一下几个重要的节点

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

  • org.apache.commons.beanutils.PropertyUtilsBean#getIntrospectionData <--

  • org.apache.commons.beanutils.PropertyUtilsBean#fetchIntrospectionData <--

  • org.apache.commons.beanutils.DefaultBeanIntrospector#introspect <--

  • java.beans.Introspector#getBeanInfo(java.lang.Class<?>)

    beanInfo = new Introspector(beanClass, null, USE_ALL_BEANINFO).getBeanInfo();
    
    在创建 `Introspector` 对象时, 会递归获取class的超类,也就是说超类中的属性也会包含进来, 构造方法中,调用了下面的方法 `findExplicitBeanInfo` , 这里实际上借用的是jdk的 `BeanInfoFinder#find()` 方法
    
    /**
     * 
     */
    private static BeanInfo findExplicitBeanInfo(Class<?> beanClass) {
        return ThreadGroupContext.getContext().getBeanInfoFinder().find(beanClass);
    }
    
    

3. 属性拷贝

上面通过内省获取了Bean对象的基本信息(成员变量 + 读写方法), 剩下的一个点就是源码中的 copyProperty(dest, name, value); 实际的属性值设置

看代码中,用了很多看似高大上的东西,排除掉一些不关心的,主要干的就是这么几件事情

  • 属性描述对象 descriptor = getPropertyUtils().getPropertyDescriptor(target, name);
  • 参数类型 type = descriptor.getPropertyType();
  • 属性值的类型转换 value = convertForCopy(value, type);
  • 属性值设置 getPropertyUtils().setSimpleProperty(target, propName, value);

最后属性设置的源码如下, 删了很多不关心的代码,基本上和我们上面的实现相差不大

public void setSimpleProperty(Object bean,
                                         String name, Object value)
            throws IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {

        // Retrieve the property setter method for the specified property
        PropertyDescriptor descriptor =
                getPropertyDescriptor(bean, name);

        Method writeMethod = getWriteMethod(bean.getClass(), descriptor);

        // Call the property setter method
        Object[] values = new Object[1];
        values[0] = value;

        invokeMethod(writeMethod, bean, values);

    }

4. 小结

apache的BeanUtils实现属性拷贝的思路和我们上面的设计相差不多,那么差距在哪 ? 仔细看 BeaUtils 源码,发现有很多优化点

  • 获取 clas对应的 BeanInfo 用了缓存,相当于一个class只用反射获取一次即可,避免每次都这么干
  • 类型转换,相比较我们上面原始到爆的简陋方案,BeanUtils使用的是专门做类型转换的 Converter 来实现,所有你可以自己定义各种类型的转换,注册进去后可以实现各种鬼畜的场景了
  • 各种异常边界的处理 (单反一个开源的成熟产品,这一块真心没话说)
  • DynaBean Map Array 这几个类型单独进行处理,上面也没有分析
  • 用内省来操作JavaBean对象,而非使用反射 参考博文《深入理解Java:内省(Introspector)》

转载于:https://my.oschina.net/u/566591/blog/897620

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值