背景
经历了n多次面试之后,每个面试官问必问的一个问题,就是问我用过反射吗,我当时只是颤颤巍巍的回答说具体没有使用过,但是Spring的IOC容器就是根据配置文件信息获取类文件信息进而反射创建bean实例,然后成功把面试官带领到我的八股文阵地,balabalabala说一堆,然后结束。
但是,真的心里踏实吗?最近工作中也是遇到了一个场景需要将实体类转为map,好家伙,不就是反射获取属性信息,执行get方法,然后放到map里面嘛,这不,反射不就用上了嘛,虽然已经工作了,但是还是给之前的自己一个答案吧!
关于反射的介绍详细见另一篇博客,看完详细介绍再看Bean转Map才会不懵逼:点击查看反射详细介绍
Bean转Map
下面问题就是Bean转Map了,先完成功能再考虑性能。
我们咋把一个Bean转成Map呢,需要获取所有的属性,然后把每个属性值都放入map中,还得一一对应,简单来说,先获取所有字段,然后获取所有方法,把字段和方法进行匹配,匹配之后呢,就可以执行方法,然后把结果放入map和字段对应,就结束了,毫无疑问,得用双循环,复杂度O(n^2)
话不多说,上代码!
/**
* 双循环版本 循环解析1000000(100w)次耗时20s
* beanmap 0s
* @param o
* @return
*/
public static Map<Object, Object> classToMap20s(Object o) {
HashMap<Object, Object> map = new HashMap<>();
Class<?> clazz = o.getClass();
// 所有字段已经拿到!
Field[] fields = clazz.getDeclaredFields();
// 所有方法拿到
Method[] methods = clazz.getDeclaredMethods();
// 字段方法匹配
for (Field field : fields) {
for (Method method : methods) {
String methodName = method.getName();
String fieldName = field.getName();
if (methodName.contains("get") && StringUtils.upperCase(methodName).contains(StringUtils.upperCase(fieldName))) {
try {
map.put(field.getName(), method.invoke(o));
} catch (InvocationTargetException | IllegalAccessException e) {
// InvocationTargetException 被调用的方法内部可能有异常抛出没有被捕获,就报这个
// IllegalAccessException 没有对这个类的访问权限的时候报这个异常
e.printStackTrace();
}
}
}
}
return map;
}
总体思路就是拿到所有字段和方法,然后匹配执行对应方法,把值放入map。
现在功能是实现了,总不能拿个O(n^2)的在项目里跑吧 = =!
所以要考虑着手优化,咋优化呢?思考一下,我们双循环的根源,就是我们拿到一个实体类,不知道这个实体类的字段和方法对应信息,所以才需要双循环解析,对于重复的实体类,都做了重复的解析!
so!把字段和方法的对应关系存到缓存map里面,然后再次解析同一个实体类的时候,就可以直接拿到对应关系,直接一个一个执行方法然后赋值进结果map就好了!话不多说开搞,代码如下:
/**
*
* 双循环优化,使用map缓存
* 循环解析1000000(100w)次耗时1s
* beanMap 0s
* 解析1000w次耗时12s
* beanMap 1s
* @param o
* @return
*/
public static Map<Object, Object> classToMap(Object o) {
HashMap<Object, Object> map = new HashMap<>();
Class<?> clazz = o.getClass();
// 所有字段已经拿到!
Field[] fields = clazz.getDeclaredFields();
// 所有方法拿到
Method[] methods = clazz.getDeclaredMethods();
// 缓存没打到,进行字段方法匹配
if (!cacheMap.containsKey(clazz)) {
Map<String, Method> fieldMethodMap = new HashMap<>();
// 字段方法匹配
for (Field field : fields) {
for (Method method : methods) {
String methodName = method.getName();
String fieldName = field.getName();
if (methodName.contains("get") && StringUtils.upperCase(methodName).contains(StringUtils.upperCase(fieldName))) {
// 匹配好了放缓存里下次使用
fieldMethodMap.put(fieldName, method);
try {
map.put(field.getName(), method.invoke(o));
} catch (InvocationTargetException | IllegalAccessException e) {
// InvocationTargetException 被调用的方法内部可能有异常抛出没有被捕获,就报这个
// IllegalAccessException 没有对这个类的访问权限的时候报这个异常
e.printStackTrace();
}
}
}
}
// 匹配结束了放入缓存
cacheMap.put(clazz, fieldMethodMap);
} else {
// 缓存打到了,直接拿出类信息执行方法赋值
Map<String, Method> fieldMethod = cacheMap.get(clazz);
for (String field : fieldMethod.keySet()) {
try {
map.put(field, fieldMethod.get(field).invoke(o));
} catch (InvocationTargetException | IllegalAccessException e) {
// InvocationTargetException 被调用的方法内部可能有异常抛出没有被捕获,就报这个
// IllegalAccessException 没有对这个类的访问权限的时候报这个异常
e.printStackTrace();
}
}
}
return map;
}
跑他个100w次,从20s到1s,从O(n^2) 到 O(n),拿捏了!
总结:Bean转Map几种方法
- bean转json,json再解析成map,比较绕,比较耗时,不推荐
import com.alibaba.fastjson.JSON; Map map = JSON.parseObject(JSON.toJSONString(javaBean), Map.class);
- 使用反射,效率次之,比较麻烦,可以自己造轮子搞一下,但是尽量别重复造轮子,造轮子是为了学习!
- 使用hutool工具包里面的BeanUtil,效率和咱们自己写的反射+缓存效率差不多,使用挺简单,项目中引入这个工具包的直接用就好,挺强大的工具包
import cn.hutool.core.bean.BeanUtil; Map<String, Object> beanToMap = BeanUtil.beanToMap(screenNumPo);
- 【重点】使用beanMap,效率高,用了缓存,不费时间,还简单
import org.springframework.cglib.beans.BeanMap; BeanMap beanMap = BeanMap.create(screenNumPo); // 可以看下BeanMap源码,里面用了WeakHashMap进行缓存,用的真是恰到好处 private static volatile Map<ClassLoader, AbstractClassGenerator.ClassLoaderData> CACHE = new WeakHashMap();
最后还是推荐大家使用BeanMap,效率贼高,使用简单,我也在项目中用了BeanMap做了最后解决方案~