背景
项目使用了ES来替换MySQL的查询,然而在ES的实体定义过程中,我们选择了一个索引多Nested的形式来存储数据,这样就带来了一个结果就是查询出来的ES结果被打散到各个Nested的集合中去了。同时为了降低改造的耦合度,和前端的接口定义并没有修改,这就需要将层级的ES结果再打散到扁平的实体中去。因为映射的字段不同,甚至于映射的层级也不同,BeanUtils#copyProperties()方法并不适用,同时由于我们虽然使用了Nested类型,但是Nested下的List中的元素最多只有一个(业务上可以限定住)。因此这里简单造了个轮子来实现该方法。
实体VO
如下是ES的model,其中的Student和Course都是Nested类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Document(indexName = "index",type = "type") public class BasicDataResult implements Serializable { private static final long serialVersionUID = 1L; @Field(type = FieldType.Nested) private List<Student> student; @Field(type = FieldType.Nested) private List<Course> course; }
class Student{ private Integer id; private Integer name; }
class Course{ private Integer id; private Integer name; }
|
那么其查询结果是形如下面的json
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "student": [ { "id": 1, "name": "echo" } ], "course": [ { "id": 1, "name": "java" } ] }
|
而我们正常和前端约定好的返回结果可能是这个样子
1 2 3 4 5 6
| { "studentId":1, "studentName":"echo", "courseId":1, "courseName":"java" }
|
显然,其对应的实体就是酱样
1 2 3 4 5 6
| public class Info{ private Integer studentId; private Integer courseId; private String studentName; private String courseName; }
|
实现思路
参照BeanUtils#copyProperties(),以及Mybatis的ResultMap的实现方式,显然反射是一个很奈斯的选项。
代码
首先,既然是不同名称的字段的对照规则,自然需要有一张对照表来告诉机器字段间的对照关系。这样的对照表很自然的就想到了map来存储。这里定义了一个抽象父类AbstractConvertRule
1 2 3
| public abstract class AbstractConvertRule { public abstract Map<String,String> getConvertMap(); }
|
又因为这里的需求是从ES的Nested结构到扁平实体,因此又定义了一个抽象父类ElasticNestedConvertRule来提供一些默认的实现。
1 2 3 4 5 6 7 8
| public abstract class ElasticNestedConvertRule extends AbstractConvertRule { protected static String getStudent() { return "Student."; } protected static String getCourse() { return "Course."; } }
|
ok,下面就是最终的映射实体类了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public class BasicDataToInfoRule extends ElasticNestedConvertRule{ private static Map<String,String> map = new HashMap<>(); private static final String STUDENT = getStudent(); private static final String COURSE = getCourse(); static { map.put("studentId",STUDENT + "id"); map.put("courseId",COURSE + "id"); map.put("studentName",STUDENT + "name"); map.put("courseName",COURSE + "name"); } @Override public Map<String,String> getConvertMap() { return map; } }
|
既然对照关系出来了,剩下的就是通过反射来将A实体中的值映射到B实体中去了,下面是转换的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public static void convertData(Object setBean, Object getBean, Map<String,String> convertData) throws Exception { Field[] fields = setBean.getClass().getDeclaredFields(); for (Field field : fields) { String fieldName = field.getName(); if(convertData.containsKey(fieldName)){ Object value = null; String[] split = convertData.get(fieldName).split("\\."); if(split.length != 2){ throw new InvalidPropertiesFormatException("convertData中,key值为:"+fieldName+"的value值不合法,请检查"); } Object tableBean = dynamicGet(getBean, StringUtils.toLowerCaseFirstOne(split[0])); if(tableBean instanceof ArrayList){ if(((ArrayList) tableBean).isEmpty()){ continue; } List<?> list = castObjectToList(tableBean); Object fieldBean = list.get(0); value = dynamicGet(fieldBean,split[1]); }else { value = tableBean; } dynamicSet(setBean,fieldName,value); } } }
|
下面列了一些#convertDate中用到的私有方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public static Object dynamicGet(Object obj,String propertyName) throws NoSuchFieldException,IllegalAccessException{ try { Field field = obj.getClass().getDeclaredField(propertyName); field.setAccessible(true); return field.get(obj); } catch (NoSuchFieldException e) { throw new NoSuchFieldException(String.format("实体:%s中不包含:%s字段,请检查对照规则",obj,propertyName)); } catch (IllegalAccessException e) { throw new IllegalAccessException(String.format("实体:%s中%s字段的不可见,请检查",obj,propertyName)); } }
public static void dynamicSet(Object obj,String propertyName,Object value) throws NoSuchFieldException,IllegalAccessException{ try { Field field = obj.getClass().getDeclaredField(propertyName); if(field.getType() == Long.class && value instanceof Integer){ value = ((Integer) value).longValue(); }else if(field.getType() == Integer.class && value instanceof Byte){ value = ((Byte) value).intValue(); } field.setAccessible(true); field.set(obj,value); } catch (NoSuchFieldException e) { throw new NoSuchFieldException(String.format("实体:%s中不包含:%s字段,请检查对照规则",obj,propertyName)); } catch (IllegalAccessException e) { throw new IllegalAccessException(String.format("实体:%s中%s字段的不可见,请检查",obj,propertyName)); } }
private static <T extends List<?>> T castObjectToList(Object obj){ return (T) obj; }
|
至此,工具类就算完工了,剩下的就是调用了,其调用方式就喝BeanUtils#copyProperties差不多了,只不过注意是参数反过来了
1 2 3 4
| BasicDataToInfoRule rule = new BasicDataToInfoRule(); Map<String,String> convertMap = rule.getConvertMap(); Info info = new Info(); BaseConvert.convertData(info,basicDataResult,convertMap);
|
后记
再之后的需求中,又遇到了将带下划线的实体映射到驼峰的实体中去,不过好在本次是从一个扁平的实体到另一个扁平的实体中,因此继承抽象规则AbstractConvertRule类覆盖其#getConvertMap
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class UnderLineToCamelRule extends AbstractConvertRule{ private static Map<String,String> map = new HashMap<>(); static{ map.put("s_id","sid"); map.put("s_name","sName"); } @Override public Map<String, String> getConvertMap() { return map; } }
|
而后在BaseConvert的工具类中增加了对扁平实体的对照方法#convertVO,因为不需要处理层级关系,代码简单很多
1 2 3 4 5 6 7 8 9 10
| public static void convertVO(Object setBean,Object getBean,Map<String,String> convertMap) throws Exception{ Field[] fields = setBean.getClass().getDeclaredFields(); for (Field field : fields) { String fieldName = field.getName(); if(convertMap.containsKey(fieldName)) { Object value = dynamicGet(getBean, convertMap.get(fieldName)); dynamicSet(setBean,fieldName,value); } } }
|
感想
- 平时在实现一个工具类的过程中,最好提供足够优秀的可扩展性
- 良好设计模式的重要性