起源和问题
这两天写了一个很让人头疼的东西,手上的项目对接一个银行的系统,看了他们的接口文档发现,他们要求我们用表单形式进行参数传递(其实就是键值对),这个还好,后面跟着是如果是list,那就是形如这样:key=user[0].name,value=“张三”;如果是map,那就是:key=user[name],value=“张三”,这种表达式其实很常见,常见的模板引擎里经常看到,比如el表达式不就差不多长这样嘛。当时问题是,我怎么把一个java对象转换成一组这样格式的表达式呢。一开始我觉得这种东西其实挺常见的,就在网上搜,找了一下午没找到(绝望),还去一些java群里问了,也没人回答,还有人冒出来说这不就是json嘛(老子真的是,要是是json,遍地都是工具包,老子还发群里来问?)
找了一下午没找到现成的,最后心一横决定自己写了,就当是letcode上的一到算法题吧。
对象转键值对组
分析
其实对象转换成一组键值对,难点在于key,value其实就是变量的值,我们就拿下面这个类为例:
@Data
public class PaserJSON {
private User user1;
private String test;
private List<User> users;
private Map<String,User> userMap;
private Map<String,String> stringMap;
private List<List<User>> listList;
@Data
public static class User {
private String username;
private String hiheih;
private boolean flag;
private Boolean flag2;
private int val1;
private Integer val2;
}
}
这个类也是我做测试的类,其实已经挺复杂了,里面最简单的肯定是String test这个变量,转换成键值对就是key=test,value=“xxx”(xxx变量的值)。比较复杂的,比如users往下的一个变量,转换成键值对就是key=users[0].username,value=“xxx”,如果是userMap就是,key=userMap[张三].username,value=“xxx”,还有最后那个listList,key=listList[0][0].username,value=“xxx”。分析下来,基调就定下来了,其实就是一个递归,对最外层的属性进行一个递归,一层一层往里面走,一直走到不可拆为止。
这么看,其实不是特别难。但有几个关于java的知识点:
- 怎么获取到一个对象的每个属性的值,以及变量名,以及类型。其实很容易想到就是反射:
Field[] fields = o.getClass().getDeclaredFields();
o就是一个对象,这样便能获取到一个类里面所有的属性,以及属性的变量名,属性类型,如果甚至可以直接拿到属性的值,但是如果是私有类型,会破坏类,所以我们后面采用了反射调用get方法来获取属性值。
- 怎么确定一个对象不可再分?比如说上面这个类User,你看到这个User你怎么知道还要往里面走?你可能会说通过getDeclaredFields,直到拿不到Filed[],如果是基本数据类型是正确的,你调int.class.getDeclaredFields(),你会发现拿不到Field了。但是如果是String、Integer之类的呢?亲自测了一下:
public static void main(String[] args) {
Field[] fields =Integer.class.getDeclaredFields();
for(Field field:fields){
System.out.println(field.getName());
}
}
打印结果:
MIN_VALUE
MAX_VALUE
TYPE
digits
DigitTens
DigitOnes
sizeTable
value
SIZE
BYTES
serialVersionUID
原来Integer里有这么多属性。所以这一招是行不通的,你可以说你对象里不用Integer全用int,但你说不用String,那也太难了。后来决定以这个为边界:如果是自己定义的类,那就是可拆的,如果是java自带的一些类那就不可拆,直接toString转字符串(当然List,Map这些集合除外)。那如何区分一个类是自己写的还是java自带的?如下:
private static boolean isJavaClass(Class<?> clz) {
return clz != null && clz.getClassLoader() == null;
}
如果不理解,可以去翻一下我jvm专题下,类加载机制那一章就懂了,如果不懂类加载机制,可能确实看不懂这一行代码(这行代码还不是我写的,还是我网上找到了,直到看到之后才恍然大悟,之前自己也不知道怎么区分)。
- 如果一个类是java自带的类,如何判断他是否为集合呢,Class类里的isAssignableFrom这个方法能够判断,一个类是否为你的父类,比如:
List.class.isAssignableFrom(ArrayList.class); \\true
Map.class.isAssignableFrom(HashMap.class); \\true
ArrayList.class.isAssignableFrom(List.class); \\false
HashMap.class.isAssignableFrom(List.class); \\false
判断前面的一个类是否为后面的一个类的父类。其实我们用的最多的也就是这两个:List和Map。
实现
通过上面的分析,那最终的代码也就出来了:
/**
* 将对象转换成map
*
* @param prefix
* @param o
* @return
*/
public static Map<String, String> getFiledsInfo(String prefix, Object o) {
Map<String, String> params = new HashMap<>();
if (null == o) {
return new HashMap<>();
}
Class clazz = o.getClass();
if (isJavaClass(clazz)) {
if (Map.class.isAssignableFrom(clazz)) {
Map<String, Object> map = (Map) o;
for (Map.Entry<String, Object> val : map.entrySet()) {
String childPrefix = prefix + "[" + camelToUnderline(val.getKey(), 1) + "]";
params.putAll(getFiledsInfo(childPrefix, val.getValue()));
}
} else if (List.class.isAssignableFrom(clazz)) {
List<Object> list = (List) o;
int index = 0;
for (Object val : list) {
String childPrefix = prefix + "[" + index + "]";
params.putAll(getFiledsInfo(childPrefix, val));
index++;
}
} else {
params.put(prefix, o.toString());
}
} else {
Field[] fields = o.getClass().getDeclaredFields();
for (Field field : fields) {
String childPrefix = prefix + (StringUtils.isEmpty(prefix) ? "" : ".") + camelToUnderline(field.getName(), 1);
params.putAll(getFiledsInfo(childPrefix, getFieldValueByName(field.getName(), o, field.getType())));
}
}
return params;
}
/**
* 判断一个类是JAVA类型还是用户定义类型
*
* @param clz
* @return
*/
private static boolean isJavaClass(Class<?> clz) {
return clz != null && clz.getClassLoader() == null;
}
/**
* 根据属性名获取属性值
*/
private static Object getFieldValueByName(String fieldName, Object o, Class clazz) {
try {
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String getter = "get" + firstLetter + fieldName.substring(1);
if (clazz.equals(boolean.class)) {
getter = "is" + firstLetter + fieldName.substring(1);
}
Method method = o.getClass().getMethod(getter, new Class[]{});
Object value = method.invoke(o, new Object[]{});
return value;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 驼峰赚下划线
*
* @param param 驼峰字符
* @param charType 统一转大写
* @return
*/
public static String camelToUnderline(String param, Integer charType) {
if (param == null || "".equals(param.trim())) {
return "";
}
int len = param.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = param.charAt(i);
if (Character.isUpperCase(c)) {
sb.append(UNDERLINE);
}
if (charType == 2) {
sb.append(Character.toUpperCase(c)); //统一都转大写
} else {
sb.append(Character.toLowerCase(c)); //统一都转小写
}
}
return sb.toString();
}
由于银行文档里的字段全是下划线格式,而我们的公司命名规则全是驼峰形式的,所以这里面加了驼峰到下换线转换的代码。
键值对组字符串转对象
一开始以为上面那个很难,其实写起来发现还好,但是下面这个真的有点难。有点可惜的我花了大功夫把这个实现了出来,最后发现行方要求我们这样给他们传对象,他们给我们传对象到时毫不含糊的传了个json过来,所以最后我又换回了json工具包。这都是后话了。
分析
其实键值对转回对象就是上面的逆过程,但是处理的时候就遇到一个问题,按道理我们以"."为分界,但是写下来发现[]里面的其实也是一个节点,所以一开始把字符串处理了一下:
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey().replaceAll("]", "");
key = key.replaceAll("\\[", ".[");
}
把形如"[name]“这样的换成了”.[name"这样的来处理,然后通过"."划分节点。
类其实就是一个树状结构,我写之前在思考要不要先把键值对换成树状结构,一开始没有这么做,后来还是这样做了,两点思考:
- 如果不先转换成树结构,那每次新解析一个键值对,都要从对象的根节点往下遍历,如果转换成树状结构后,就不用每次都从根节点重新遍历了,简单估计了一下效率似乎要高一些(我也没验证过)
- 第二点是关键点,就是不转换成树状结构,list不是很好排序,必须要全部处理完之后,再遍历整个数,把是list 的进行排序。而转换成树之后,每次递归回到上一层,就可以进行排序了。
和上面对象转字符一样,这里也需要考虑几个java问题:
- 如何把一个字符串转换成任意基本类型(包括他们的包装类和String),这是值的处理。键值对里值都是字符串,但类里面,最终的值都是基本数据类型。直接强转肯定不行的,网上找了好久,没找到好的处理方法,最终自己这样实现了:
/**
* 将字符串转换成任意基本类型
*
* @param val
* @param fieldClass
* @return
*/
private static Object castString(String val, Class fieldClass) {
if (val == null || val.equals("null")) {
return null;
}
if (fieldClass.equals(int.class) || fieldClass.equals(Integer.class)) {
return Integer.parseInt(val);
}
if (fieldClass.equals(boolean.class) || fieldClass.equals(Boolean.class)) {
return Boolean.parseBoolean(val);
}
if (fieldClass.equals(float.class) || fieldClass.equals(Float.class)) {
return Float.parseFloat(val);
}
if (fieldClass.equals(double.class) || fieldClass.equals(Double.class)) {
return Float.parseFloat(val);
}
if (fieldClass.equals(char.class) || fieldClass.equals(Character.class)) {
return val.charAt(0);
}
if (fieldClass.equals(String.class)) {
return val;
}
return null;
}
- 怎么才能拿到泛型的真实类型,比如List的E,和Map<V,W>的W,V一般就是字符串:
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type childNodeClassType = parameterizedType.getActualTypeArguments()[0];
- 怎么才能拿到泛型里的泛型呢?比如List<Map<String,String>>,虽然不多见,倒也是个场景,承接上面的代码,其实第二行拿出来的Type childNodeClassType还是一个ParameterizedType类型,所以继续getActualTypeArguments就好了:
ParameterizedType parameterizedType = (ParameterizedType) nodeClassType;
Type childType = parameterizedType.getActualTypeArguments()[0];
实现
public static <T> T string2Bean(String str, Class<T> clazz) throws Exception {
Map<String, String> map = (Map<String, String>) JSON.parse(str);
Node root = new Node();
root.setKey("root");
root.setChildrenType(2);
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey().replaceAll("]", "");
key = key.replaceAll("\\[", ".[");
str2Node(key, entry.getValue(), root);
}
return node2Bean(root, clazz);
}
private static <T> T node2Bean(Node node, Class<T> clazz) throws Exception {
Object obj = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
for (Node childNode : node.getChildren()) {
for (Field childField : fields) {
if (childField.getName().equals(childNode.getKey())) {
childNode2Bean(childNode, obj, node.getChildrenType(), childField.getType(), childField);
break;
}
}
}
return (T) obj;
}
/**
* @param node 当前节点
* @param parent 当前节点的所属对象
* @param type 当前节点所属类型 1-list,2-bean
* @param nodeClassType 当前节点的数据类型:如果是父节点是list,为list泛型的真实类型,如果父节点为类,则是这个字段的类型
* @return
* @throws Exception
*/
public static void childNode2Bean(Node node, Object parent, int type, Type nodeClassType, Field field) throws Exception {
if (type == 1) {
handleList(node, parent, nodeClassType, field);
} else if (type == 2) {
handleBean(node, parent, nodeClassType, field);
} else if (type == 3) {
handleMap(node, parent, nodeClassType, field);
}
}
public static void handleMap(Node node, Object parent, Type nodeClassType, Field field) throws Exception {
Map map = (Map) parent;
if (node.getChildrenType() == 0) {
Class clazz = (Class) nodeClassType;
map.put(node.getKey(), castString(node.getValue(), clazz));
} else if (node.getChildrenType() == 1) {
ParameterizedType parameterizedType = (ParameterizedType) nodeClassType;
Type childType = parameterizedType.getActualTypeArguments()[0];
List childList = new ArrayList(node.getChildren().size());
for (Node childNode : node.getChildren()) {
childNode2Bean(childNode, childList, node.getChildrenType(), childType, null);
}
map.put(node.getKey(), childList);
} else if (node.getChildrenType() == 2) {
handleChildBean(node, map, nodeClassType, field);
} else if (node.getChildrenType() == 3) {
ParameterizedType parameterizedType = (ParameterizedType) nodeClassType;
Type childType = parameterizedType.getActualTypeArguments()[1];
Map childMap = new HashMap();
for (Node childNode : node.getChildren()) {
childNode2Bean(childNode, childMap, node.getChildrenType(), childType, null);
}
map.put(node.getKey(), childMap);
}
}
private static void handleList(Node node, Object parent, Type nodeClassType, Field field) throws Exception {
List list = (List) parent;
if (node.getChildrenType() == 0) {
Class clazz = (Class) nodeClassType;
list.add(Integer.parseInt(node.getKey()), castString(node.getValue(), clazz));
} else if (node.getChildrenType() == 1) {
ParameterizedType parameterizedType = (ParameterizedType) nodeClassType;
Type childType = parameterizedType.getActualTypeArguments()[0];
List childList = new ArrayList(node.getChildren().size());
for (Node childNode : node.getChildren()) {
childNode2Bean(childNode, childList, node.getChildrenType(), childType, null);
}
list.add(childList);
} else if (node.getChildrenType() == 2) {
handleChildBean(node, list, nodeClassType, field);
} else if (node.getChildrenType() == 3) {
ParameterizedType parameterizedType = (ParameterizedType) nodeClassType;
Type childType = parameterizedType.getActualTypeArguments()[1];
Map childMap = new HashMap();
for (Node childNode : node.getChildren()) {
childNode2Bean(childNode, childMap, node.getChildrenType(), childType, null);
}
list.add(childMap);
}
}
private static void handleBean(Node node, Object parent, Type nodeClassType, Field field) throws Exception {
if (node.getChildrenType() == 0) {
Class clazz = (Class) nodeClassType;
setFieldValueByName(node.getKey(), parent, node.getValue(), clazz);
} else if (node.getChildrenType() == 1) {
List list = new ArrayList(node.getChildren().size());
for (Node child : node.getChildren()) {
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type childNodeClassType = parameterizedType.getActualTypeArguments()[0];
childNode2Bean(child, list, node.getChildrenType(), childNodeClassType, null);
}
setFieldValueByName(node.getKey(), parent, list, List.class);
} else if (node.getChildrenType() == 2) {
handleChildBean(node, parent, nodeClassType, field);
} else if (node.getChildrenType() == 3) {
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
Type childType = parameterizedType.getActualTypeArguments()[1];
Map childMap = new HashMap();
for (Node childNode : node.getChildren()) {
childNode2Bean(childNode, childMap, node.getChildrenType(), childType, null);
}
setFieldValueByName(node.getKey(), parent, childMap, Map.class);
}
}
private static void handleChildBean(Node node, Object parent, Type nodeClassType, Field field) throws Exception {
Class clazz = (Class) nodeClassType;
Object obj = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
for (Node childNode : node.getChildren()) {
for (Field childField : fields) {
if (childField.getName().equals(childNode.getKey())) {
childNode2Bean(childNode, obj, node.getChildrenType(), childField.getType(), childField);
break;
}
}
}
if (parent instanceof List) {
((List) ((List) parent)).add(obj);
} else if (parent instanceof Map) {
((Map) parent).put(node.getKey(), obj);
} else {
setFieldValueByName(node.getKey(), parent, obj, clazz);
}
}
private static void str2Node(String key, String val, Node parentNode) {
int indexOfPoint = key.indexOf(".");
String realKey = indexOfPoint > 0 ? (key.substring(0, indexOfPoint)) : key;
boolean isOver = indexOfPoint < 0 ? true : false;
int type = 2;
int begin = realKey.indexOf("[");
String keyName = begin >= 0 ? realKey.substring(1) : realKey;
if (begin == 0 && StringUtils.isAllNumber(keyName)) {
type = 1;
} else if (begin == 0) {
type = 3;
}
parentNode.setChildrenType(type);
Node node = null;
boolean nodeIsExist = false;
if (parentNode.getChildren() != null) {
for (Node existNode : parentNode.getChildren()) {
if (existNode.getKey().equals(keyName)) {
node = existNode;
nodeIsExist = true;
}
}
}
if (!nodeIsExist) {
node = new Node();
node.setKey(underlineToCamel(keyName));
if (isOver) {
node.setValue(val);
} else {
str2Node(key.substring(indexOfPoint + 1), val, node);
}
List<Node> nodeList = parentNode.getChildren();
if (nodeList == null) {
nodeList = new ArrayList<>();
parentNode.setChildren(nodeList);
}
nodeList.add(node);
} else {
str2Node(key.substring(indexOfPoint + 1), val, node);
}
}
后记
后来实际测下来发现,银行给我们返回的时候给我们的是标准的json格式,所以最后字符串转对象,没有用上,所以如果要用这部分代码,最好自己再测试和调试一下。但换句话说回来了,为啥银行要求我们要转成这种格式给他们,他们给我们就变成了json了。或许他们发现自己实现bean转这种格式的键值对有点头秃。那大家可能互相,难道这种键值对转对象他们就不投秃了,其实这个是被springmvc给实现了的。如果我们仔细的话,在网页上通过表单网后端传数据的时候,list其实就是users[0].name类似这种格式网后端传的,而后端springmvc里,我们直接给一个对应的对象,就能接到数据了。