兰州优化网站排名中国十大互联网公司排名
Spring提供的BeanUtils源码剖析目录
- 前言
- 先导知识之JAVA内省
- 山寨版 BeanUtils.copyProperties()
- 效果展示
- 正版 BeanUtils.copyProperties() 源码剖析
- 上班一年的时候补充:Map转对象(补充)
- 抽象接口 Trans 使用
- 初版工具类的 Trans 接口存在的 bug优化!加入高亮字段映射
- 抽象接口 Trans 优化
- 抽象接口 Trans 最终效果
- 总结
- 小咸鱼的技术窝
前言
前段时间刚入职xxx公司,由于公司内部有一套自己的技术架构,并且很多东西都是自研、封装的。身为小菜鸡的在业余时间抽空看了一下里面的源码,感觉这些人基础挺扎实的哈哈哈哈,这些个基础架构工具包全是自研的,牛逼。一下子激发了我看 BeanUtils 源码的兴趣,想着靠自己的摸索,结合之前研究的JDK动态代理技术,自己也尝试着写一套功能类似的基架组件出来(结合代理技术的以后再写,这玩意有点繁琐)。
先导知识之JAVA内省
不清楚JAVA内省的小伙伴,先来了解一下这方面的知识,何为内省?内省是JAVA提供的一组可以操纵 JavaBean 属性的一组 Api,掌握了这些个 Api 我们也可以写一个自己的 BeanUtils 出来。下面先来介绍一波这些个 Api。
- Introspector.getBeanInfo(o.getClass()) 获取当类的一个BeanInfo对象,和Spring中的 BeanDefinition 对象有点类似,BeanDefinition 是封装了 Bean 信息的一个信息对象,BeanInfo 是封装了 Java 对象一系列 Api 方法的对象。
- BeanInfo.getPropertyDescriptors() 获取 PropertyDescriptor 对象,可以通过此对象获取类字段属性 name、value、get和set方法名字、对字段进行属性填充。等操作
山寨版 BeanUtils.copyProperties()
恭喜你已经通过了入职考核,有资格进入 zzh 盗版集团来,我们集团这里的人都是人中翘楚、人中龙凤、国之栋梁、梁之中流砥柱、柱中之无懈可击的存在了。下面列举一个 我司开发的山寨版BeanUtils.copyProperties()软件 给读者参观(),温馨提示:前方500米有宝箱,请大家仔细翻阅。大概流程为:遍历操作对源对象与目标对象进行属性名的匹配,匹配上了,随即进行对应属性名的value值填充。
public class IntrospectionTest {public static List<Object> init() {List<Object> zzhSources = new ArrayList<>();ZzhSource zzhSource = new ZzhSource();zzhSource.setAge("1");zzhSource.setName("zzh1");ZzhSource zzhSource2 = new ZzhSource();zzhSource2.setAge("2");zzhSource2.setName("zzh2");zzhSources.add(zzhSource);zzhSources.add(zzhSource2);return zzhSources;}@SneakyThrowspublic static void main(String[] args) {List<Object> zzhTargets = new ArrayList<>();System.err.println("copy before: " + zzhTargets);copyProperties(init(), zzhTargets, ZzhTarget.class);System.err.println("copy after:: " + zzhTargets);}public static void copyProperties(List<Object> sources, List<Object> targets, Class targetClass) {sources.stream().forEach(source -> {try {Object o = targetClass.newInstance();Arrays.stream(Introspector.getBeanInfo(source.getClass()).getPropertyDescriptors()).forEach(sp -> {try {/*** 遍历操作:targetClass 对应的对象与 sources 集合中对应的对象进行属性匹配*/Arrays.stream(Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors()).forEach(op -> {/*** 名称匹配上了之后进行属性填充*/if (sp.getName().equals(op.getName()) && !sp.getName().equals("class")) {try {/*** 对我们新创建的 o 对象进行属性填充,填充数据的来源为 sources*/op.getWriteMethod().invoke(o, sp.getReadMethod().invoke(source));} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}});} catch (IntrospectionException e) {e.printStackTrace();}});/*** o 对象属性填充完毕,添加至 targets集合中*/targets.add(o);} catch (IntrospectionException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}});}@Datastatic class ZzhSource {String name;String age;String sex;}@Datastatic class ZzhTarget {String name;String age;String sex;}
}
效果展示
可以看到数据成功的拷贝到了我们指定的 List 中了
正版 BeanUtils.copyProperties() 源码剖析
步骤流程总是惊人的相似,获取对象的 PropertyDescriptor ,然后调用内省为我们提供的各种操作 字段 属性的Api,最终达到属性拷贝的目的。
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {Assert.notNull(source, "Source must not be null");Assert.notNull(target, "Target must not be null");//获取目标对象ClassClass<?> actualEditable = target.getClass();if (editable != null) {if (!editable.isInstance(target)) {throw new IllegalArgumentException("Target class [" + target.getClass().getName() + "] not assignable to Editable class [" + editable.getName() + "]");}actualEditable = editable;}
//获取目标对象的所有 PropertyDescriptorPropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;PropertyDescriptor[] var7 = targetPds;int var8 = targetPds.length;
//遍历目标对象的 PropertyDescriptorfor(int var9 = 0; var9 < var8; ++var9) {PropertyDescriptor targetPd = var7[var9];//获取目标对象的写方法,通过此方法可以对指定字段赋值Method writeMethod = targetPd.getWriteMethod();if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {//获取源对象 PropertyDescriptorPropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());if (sourcePd != null) {//获取源对象的读方法,通过此方法可以获取对应字段的 value值Method readMethod = sourcePd.getReadMethod();if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {try {//设置一下方法操作权限,为可操作if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {readMethod.setAccessible(true);}
//获取源对象字段中的 value值Object value = readMethod.invoke(source);if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}
//为目标对象进行属性填充writeMethod.invoke(target, value);} catch (Throwable var15) {throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);}}}}}}
上班一年的时候补充:Map转对象(补充)
最近在写 Es 相关的业务,但是懒得用框架,主要是 Es 6 和 Es 7 和 Es 8 之间的 api 语法相差过大,实在是垃圾,于是自己基于原始 Api 封装了一套基于组内同时使用,其中就包括 Map 转 对象,传统写法需要一个个 Set 实在是垃圾,于是用反射封转了如下一个抽象方法。
public interface Trans<T> {abstract Class getTargetClass();default Object trans(Map<String, Object> map) throws IntrospectionException, InstantiationException, IllegalAccessException {Object o = getTargetClass().newInstance();BeanInfo beanInfo = Introspector.getBeanInfo(o.getClass());Arrays.stream(beanInfo.getPropertyDescriptors()).forEach(propertyDescriptor -> {if (!"this".equals(propertyDescriptor.getName())&&!Objects.isNull(map.get(propertyDescriptor.getName()))) {try {propertyDescriptor.getWriteMethod().invoke(o, map.get(propertyDescriptor.getName()));} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}});return o;}
}
抽象接口 Trans 使用
使用很简单每个业务对象实现 Trans 中的抽象方法即可
后续 Es 查到数据直接 new ArticleResponse().trans(hit.getSourceAsMap()) 即可得到填充好的对象,简单方便!
初版工具类的 Trans 接口存在的 bug优化!加入高亮字段映射
之前没考虑到父类属性赋值的问题,以及 Es 查到的数据源全是字符串,需按照类型进行映射,比如 Es 查到的日期是 2023-12-12 字符串类型的,但是映射实体类是 Date 接受的,这种情况优化了一下代码 如下。
主要逻辑就是根据字段类型做一个匹配映射,匹配类型对不上的进行强转,以及加入高亮字段替换!!!
/*** map 转对象* author:zzh** @param <T>*/
public interface Trans<T> {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");abstract Class getTargetClass();default PropertyDescriptor[] mergeAndDeduplicate(PropertyDescriptor[]... arrays) {Set<PropertyDescriptor> set = new HashSet<>();for (PropertyDescriptor[] array : arrays) {set.addAll(Arrays.asList(array));}return set.toArray(new PropertyDescriptor[0]);}default Object trans(Map<String, Object> map, Map<String, HighlightField> highlightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {Object o = getTargetClass().newInstance();Class tclass = getTargetClass();HashMap<String, Class> nameTypeMap = new HashMap<>();//找到父类的所有字段do {Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {field.setAccessible(true);//key:字段名称,value:字段类型nameTypeMap.put(field.getName(), field.getType());});tclass = tclass.getSuperclass();} while (!tclass.equals(Object.class));PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(map.get(propertyDescriptor.getName()))) {try {Method writeMethod = propertyDescriptor.getWriteMethod();if (null != writeMethod) {if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}Object sourceValue = map.get(propertyDescriptor.getName());Class aClass = nameTypeMap.get(propertyDescriptor.getName());//类型一致,直接赋值if (sourceValue.getClass().equals(aClass)) {writeMethod.invoke(o, highlightFields.get(propertyDescriptor.getName()) != null ?(highlightFields.get(propertyDescriptor.getName()).getFragments()[0]).toString() :map.get(propertyDescriptor.getName()));}/*** 类型不一致强转,这里可以搞个策略模式优化优化*/else {if (aClass.equals(Date.class)) {Date parse = simpleDateFormat.parse(String.valueOf(map.get(propertyDescriptor.getName())));writeMethod.invoke(o, parse);}if (aClass.equals(Integer.class)) {writeMethod.invoke(o, Integer.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));}if (aClass.equals(Long.class)) {writeMethod.invoke(o, Long.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));}if (aClass.equals(int.class)) {writeMethod.invoke(o, Integer.parseInt(String.valueOf(map.get(propertyDescriptor.getName()))));}}} else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (Exception e) {//类型不匹配
// System.err.println(propertyDescriptor.getWriteMethod());
// System.err.println(propertyDescriptor.getName());
// System.err.println(o);
// System.err.println(map.get(propertyDescriptor.getName()));e.printStackTrace();}}});return o;}
}
抽象接口 Trans 优化
又优化了一下复杂对象下高亮字段的映射!!!!!最终代码如下
/*** map 转对象* author:zzh** @param <T>*/
public interface Trans<T> {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");abstract Class getTargetClass();default PropertyDescriptor[] mergeAndDeduplicate(PropertyDescriptor[]... arrays) {Set<PropertyDescriptor> set = new HashSet<>();for (PropertyDescriptor[] array : arrays) {set.addAll(Arrays.asList(array));}return set.toArray(new PropertyDescriptor[0]);}default Object trans(Map<String, Object> map, Map<String, HighlightField> highlightFieldsSource, List<String> highLightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {Object o = getTargetClass().newInstance();Class tclass = getTargetClass();HashMap<String, Class> nameTypeMap = new HashMap<>();//找到父类的所有字段do {Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {field.setAccessible(true);//key:字段名称,value:字段类型nameTypeMap.put(field.getName(), field.getType());});tclass = tclass.getSuperclass();} while (!tclass.equals(Object.class));PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(map.get(propertyDescriptor.getName()))) {try {Method writeMethod = propertyDescriptor.getWriteMethod();if (null != writeMethod) {if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}Object sourceValue = map.get(propertyDescriptor.getName());//父类以及自己所有字段类型Class aClass = nameTypeMap.get(propertyDescriptor.getName());//字符串类型以及高亮直接赋值if (sourceValue.getClass().equals(aClass)) {writeMethod.invoke(o, highlightFieldsSource.get(propertyDescriptor.getName()) != null ?(highlightFieldsSource.get(propertyDescriptor.getName()).getFragments()[0]).toString() :map.get(propertyDescriptor.getName()));}/*** 类型不一致强转,这里可以搞个策略模式优化优化*/else {if (aClass.equals(Date.class)) {Date parse = simpleDateFormat.parse(String.valueOf(map.get(propertyDescriptor.getName())));writeMethod.invoke(o, parse);}if (aClass.equals(Integer.class)) {writeMethod.invoke(o, Integer.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));}if (aClass.equals(Long.class)) {writeMethod.invoke(o, Long.valueOf(String.valueOf(map.get(propertyDescriptor.getName()))));}if (aClass.equals(List.class)) {ArrayList<Map<String, Object>> oraginSources = (ArrayList<Map<String, Object>>) map.get(propertyDescriptor.getName());if (null != oraginSources &&0 != highlightFieldsSource.size()) {for (int i = 0; i < oraginSources.size(); i++) {for (int j = 0; j < highLightFields.size(); j++) {try {if (highlightFieldsSource.containsKey(highLightFields.get(j))) {oraginSources.get(i).put(highLightFields.get(j).split("\\.")[1],highlightFieldsSource.get(highLightFields.get(j)).getFragments()[j].toString());}} catch (Exception e) {e.printStackTrace();}}}}writeMethod.invoke(o, oraginSources);}if (aClass.equals(int.class)) {writeMethod.invoke(o, Integer.parseInt(String.valueOf(map.get(propertyDescriptor.getName()))));}}} else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (Exception e) {//类型不匹配
// System.err.println(propertyDescriptor.getWriteMethod());
// System.err.println(propertyDescriptor.getName());
// System.err.println(o);
// System.err.println(map.get(propertyDescriptor.getName()));e.printStackTrace();}}});return o;}}
抽象接口 Trans 最终效果
使用方法如下
可以看到复杂对象也成功加上高亮标签
总结
Spring为我们提供的 BeanUtils 工具类其实也就是对 内省 Api中的一些方法的包转而已,思考:内省的实现原理是什么呢?我们是否可以从 Jdk 或者 CgLib动态代理入手,自己写一套 山寨版 内省 Api呢?答案肯定是可以的,以后有时间再去研究一下这个,读者感兴趣的话,也可以尝试着自己写一套内省 Api哦
小咸鱼的技术窝
关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页