文档章节

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

小灰灰Blog
 小灰灰Blog
发布于 2017/05/11 16:44
字数 2723
阅读 131
收藏 1

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

© 著作权归作者所有

共有 人打赏支持
小灰灰Blog
粉丝 183
博文 187
码字总数 326577
作品 0
武汉
程序员
私信 提问
老司机带你深入浅出Java反射

反射,它就像是一种魔法,引入运行时自省能力,赋予了 Java 语言令人意外的活力,通过运行时操作元数据或对象,Java 可以灵活地操作运行时才能确定的信息 这里笔者就深入浅出总结下Java反射,...

小刀爱编程
11/07
0
0
Java中的反射|SquirrelNote

前言 本篇简介: 反射概述 反射具体功能实现 Android中的反射应用 一、反射(Reflection)概述 1.定义 是指在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;并且对于任何一个...

跳动的松鼠
2017/11/28
0
0
转eclipse failed to create the java virtual machine

解决方法: 1.问题现象 2.java虚拟机初始化失败!寻找eclipse解压路径 3.寻找eclipse初始化文件 4.右击打开(此处以ultraedit打开为例) 5.打开寻找初始化。虚拟机为java永久生成对象(Perma...

JackMo2015
2015/11/12
0
0
Java核心机制:反射机制的原理及应用方法

一、java的核心机制 java有两种核心机制:java虚拟机(JavaVirtual Machine)与垃圾收集机制(Garbage collection): 1、Java虚拟机:是运行所有Java程序的抽象计算机,是Java语言的运行环境,在...

Java架构资源分享
11/25
0
0
Java反射实践:从反射中理解class

写在前面 今天在需求评审的时候,遇到了挺有意思的要求。需求是什么样子就不说了。总之完成这个需求需要一个调用系统api的操作。然而这个api因为并不稳定的原因。被谷歌hide掉了。 这个时候我...

MDove
04/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

开源软件会被云杀死吗 ?

本文转载云头条,原作者:Michael Stiefel是Reliable Software公司的负责人,是一名软件架构和开发顾问。 文章要点 虽然开源开发不会消失,但商业开源厂商的未来不是很有希望。随着全面管理的...

linuxCool
20分钟前
0
0
OSChina 周三乱弹 —— 谈什么对象?睡什么觉?

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @胖达panda :最肯忘却古人诗,最不屑一顾是相思。分享童丽的单曲《红豆生南国》: 《红豆生南国》- 童丽 手机党少年们想听歌,请使劲儿戳(这...

小小编辑
24分钟前
43
3
stylus

stylus基础教程,stylus实例教程,stylus语法总结

miaojiangmin
今天
3
0
PHP生成CSV之内部换行

当我们使用PHP将采集到的文件内容保存到csv文件时,往往需要将采集内容进行二次过滤处理才能得到需要的内容。比如网页中的换行符,空格符等等。 对于空格等处理起来都比较简单,这里我们单独...

豆花饭烧土豆
今天
2
0
使用 mjml 生成 thymeleaf 邮件框架模板

发邮件算是系统开发的一个基本需求了,不过搞邮件模板实在是件恶心事,估计搞过的同仁都有体会。 得支持多种客户端 支持响应式 疼彻心扉的 outlook 多数客户端只支持 inline 形式的 css 布局...

郁也风
今天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部