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)》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏jeremy的技术点滴

py3_cookbook_notes_01

3358
来自专栏跟着阿笨一起玩NET

VB.NET自我总结语法

1171
来自专栏算法修养

PAT 甲级 1104 sum of Number Segments

1104. Sum of Number Segments (20) 时间限制 200 ms 内存限制 65536 kB 代码长度限制 16...

2835
来自专栏about云

Scala的map实现key和value排序及各种排序比较等知识讨论

问题导读 1.map能否直接排序? 2.如何转换,才能排序? 3.排序结果可以存储在哪两个集合中? 4._*如何使用? 5.排序函数中,哪个可以进行升序和降序...

3948
来自专栏深度学习计算机视觉

蓝桥杯试题算法学习笔记一题目

题目 1、熊怪吃核桃 森林里有一只熊怪,很爱吃核桃。不过它有个习惯,每次都把找到的核桃分成相等的两份,吃掉一份,留一份。如果不能等分,熊怪就会扔掉一个核桃再分。...

5206
来自专栏跟着阿笨一起玩NET

VB.NET语法小结

2712
来自专栏菩提树下的杨过

java:POI导出excel

POI是一个开源项目,专用于java平台上操作MS OFFICE,企业应用开发中可用它方便导出Excel. 下面是使用示例: 1、maven中先添加依赖项 1 ...

3355
来自专栏一个会写诗的程序员的博客

《Kotlin极简教程》第五章 Kotlin面向对象编程(OOP)一个OOP版本的HelloWorld构造函数传参Data Class定义接口&实现之写pojo bean定一个Rectangle对象封

We frequently create a class to do nothing but hold data. In such a class some s...

2344
来自专栏开发与安全

散列表(二):冲突处理的方法之链地址法的实现(哈希查找)

首先需要澄清的一点是,这里讲的是hash table/hash map ,即数据项所存储的表要用数组来实现。 一、链地址法 这种基本思想:将所有哈希地址为i 的...

2630
来自专栏一个会写诗的程序员的博客

《Kotin 极简教程》第7章 面向对象编程(OOP)(2)《Kotlin极简教程》正式上架:

在上面的代码中,我们通过向注解类添加元注解(meta-annotation)的方法来指定其他属性:

1662

扫码关注云+社区

领取腾讯云代金券