能兼容一定异常的JSONUtils

63 阅读6分钟
  • 之前在生产环境遇到一个问题,日志信息在MySQL中存储时,其中有个字段是JSON,后续有需求从日志中将此JSON转换成对象来使用;但没想到数据库中有几条JSON是测试人员添加的,或者由于其他不可抗力原因导致JSON的格式未必正确,导致出现JsonProcessingException使得接口响应失败(捕获这个异常后用默认值继续执行应该也合理)。我的想法是做一个兼容性更高的JSONUtil,可以将破损的JSON中有效信息提取。

  • 项目立项原因可能并不完全合理,因为生产环境可能会对异常信息要求更加敏感,会刻意关注报错信息,或者说生产环境根本不可能有未校验的异常JSON; 但在日志系统这种优先快速写入,对统计信息的重视比对异常信息重要的场景下,能够避免由于业务无关问题导致的异常似乎也挺合理;总之其实就是自己随便写的一个小玩具。

整体的策略是这样的:

一级优化方案:尝试从对应数据库表的上下字段获取正确的JSON,通过将异常JSON投影到正确的JSON槽位上,来尽可能的获取更多信息; 二级优化方案:从转化的目标对象入手,如果JSON中出现和目标对象正确匹配的key则将value赋值上去;具体而言当对象的外层属性和子对象的属性重名,需要有判断的策略; 三级方案:最普通的JSON转化:从头到尾按照正常JSON转化的逻辑进行匹配,成功一个属性算一个属性,一旦匹配失败,返回成功的部分;

一级:

public class FormHelper {
    //尝试获取到数据库上下正确的JSON,通过将异常JSON合理投影到正确的JSON槽上,来尽量获取更多信息
    public static LinkedHashMap<String, Object> getDataFromDB(String tableName, String field, String JSON) {
        LinkedHashMap<String, Object> rightMap = tryGetCurrentForm(tableName, field);
        return compareAndSet(rightMap, JSON);
    }

    private static LinkedHashMap<String, Object> tryGetCurrentForm(String tableName, String field) {
        LinkedHashMap<String, Object> result = null;

        String url = "jdbc:mysql://192.168.56.10/demo";
        String username = "root";
        String password = "root";

        ObjectMapper objectMapper = new ObjectMapper();
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");

            Connection connection = DriverManager.getConnection(url, username, password);
            // 创建 SQL 语句
            String query = "SELECT " + field + " FROM " + tableName;

            // 创建 Statement 对象
            Statement statement = connection.createStatement();
            // 执行查询
            ResultSet resultSet = statement.executeQuery(query);
            // 将查询结果放入 LinkedHashMap
            while (resultSet.next() && result == null) {
                try {
                    String temp = (String) resultSet.getObject(1); // 假设查询结果只有一列
                    LinkedHashMap<String, Object> jsonData =
                            (LinkedHashMap) objectMapper.readValue(temp, Map.class);
                    result = jsonData;
                } catch (JsonProcessingException e) {
                    continue;
                }
            }
            if (result == null) {
                return null; //没找到合适的JSON
            }
        } catch (SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return result;
    }

    private static LinkedHashMap<String, Object> compareAndSet(LinkedHashMap<String, Object> template, String targetJSON) {
        //通过和已知正确的JSON对应的LinkedHashMap对比,尽量将信息填充到新LinkedHashMap中
        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
        String[] split = targetJSON.split("[{},]");
        int splitIndex = 0;
        for (Map.Entry<String, Object> entry : template.entrySet()) {
            //linkedHashMap为什么不提供将全部有序内容放到有序集合中的方法???
            Object obj = entry.getValue();
            if (obj instanceof LinkedHashMap<?, ?>) {
                //是JSON子对象
                String[] next = new String[split.length - splitIndex];
                if (split.length - splitIndex >= 0)
                    System.arraycopy(split, splitIndex, next, 0, split.length - splitIndex);
                LinkedHashMap<String, Object> sonMap = compareAndSet((LinkedHashMap<String, Object>) obj, next);
                map.put(entry.getKey(), sonMap);
                int sonSize = sonMap.size();
                splitIndex += sonSize;
            } else {
                if (matched(split[splitIndex++], entry.getKey())) {
                    map.put(entry.getKey(), entry.getValue());
                } else break;
            }
        }
        return map;
    }

    private static LinkedHashMap<String, Object> compareAndSet(LinkedHashMap<String, Object> template, String[] split) {
        //通过和已知正确的JSON对应的LinkedHashMap对比,尽量将信息填充到新LinkedHashMap中
        LinkedHashMap<String, Object> map = new LinkedHashMap<>();
        int splitIndex = 0;
        for (Map.Entry<String, Object> entry : template.entrySet()) {
            //linkedHashMap为什么不提供将全部有序内容放到有序集合中的方法???
            Object obj = entry.getValue();
            if (obj instanceof LinkedHashMap<?, ?>) {
                //是JSON子对象
                String[] next = new String[split.length - splitIndex];
                if (split.length - splitIndex >= 0)
                    System.arraycopy(split, splitIndex, next, 0, split.length - splitIndex);
                LinkedHashMap<String, Object> sonMap = compareAndSet((LinkedHashMap<String, Object>) obj, next);
                map.put(entry.getKey(), sonMap);
                int sonSize = sonMap.size();
                splitIndex += sonSize;
            } else {
                if (matched(split[splitIndex++], entry.getKey())) {
                    map.put(entry.getKey(), entry.getValue());
                } else break;
            }
        }
        return map;
    }

    public static boolean matched(String originJSON, String template) {
        int startIndex = originJSON.indexOf(template);
        int midIndex = originJSON.indexOf(":");
        return startIndex != -1 && midIndex != -1 && midIndex > startIndex;
    }
}

测试(如果数据库表上下文中有正确的JSON):

//TODO

二级:


public class ObjectMatcher<T> {
    //根据类来匹配破损JSON

    static List<String> keys;
    static List<List<String>> attrs;

    public T match(String json, Class<T> clazz) {
        try {
            attrs = getFieldsFromClass(clazz);
            T obj = clazz.getDeclaredConstructor().newInstance();
            Field[] fields = clazz.getDeclaredFields();
            Pattern pattern = Pattern.compile(""([^"]*?)"");
            Matcher matcher = pattern.matcher(json);

            while (matcher.find()) {
                String rowKey = matcher.group(1);
                keys.add(rowKey.substring(1, rowKey.length() - 2));
            }
            for (Field field : fields) {
                String key = field.getName();
                String value = getValueFromJson(json, key);
                if (value != null) {
                    setFieldValue(obj, field, value);
                }
            }
            return obj;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private String getValueFromJson(String json, String key) {
        //为了避免由外层 name 错误匹配到内层 name,
        // 需要一个策略<根据上下文有效内容判断所属>:判断 name 前k个key里有无引用类型的key,有则表明是内层的
        Pattern pattern = Pattern.compile(""" + key + "":"?([^"]+)"?");
        Matcher matcher = pattern.matcher(json);

        //遍历attrs属性列表,对于里面有name的外层引用类型子对象,判断在破损JSON中,里子对象key的距离
        while (matcher.find()) {
            int startIndex = matcher.start();
            dode:
            for (List<String> list : attrs) {
                String outerAttr = list.get(0);
                int outerAttrStart = json.indexOf(outerAttr);
                for (int i = 1; i < list.size(); i++) {
                    String str = list.get(i);
                    if (str.equals(key)) {
                        if (outerAttrStart - startIndex < 50) {
                            break dode;
                        }
                    } else {
                        String outerOne = matcher.group(1);
                        return outerOne.split(":")[1];
                    }
                }
            }
        }
        return null;
    }

    private List<List<String>> getFieldsFromClass(Class<?> clazz) {
        List<List<String>> allFields = new ArrayList<>();
        getFieldsRecursively(clazz, allFields, new ArrayList<>());
        return allFields;
    }

    private static void getFieldsRecursively(Class<?> clazz, List<List<String>> allFields, List<String> prefix) {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Class<?> fieldType = field.getType();
            List<String> fieldPath = new ArrayList<>(prefix);
            fieldPath.add(field.getName());
            if (fieldType.isPrimitive() || fieldType.equals(String.class)) {
                allFields.add(fieldPath);
            } else {
                getFieldsRecursively(fieldType, allFields, fieldPath);
            }
        }
    }

    private void setFieldValue(T obj, Field field, String value) {
        try {
            field.setAccessible(true);
            Class<?> fieldType = field.getType();
            if (fieldType == String.class) {
                field.set(obj, value);
            } else if (fieldType == int.class || fieldType == Integer.class) {
                field.set(obj, Integer.parseInt(value));
            } else if (fieldType == long.class || fieldType == Long.class) {
                field.set(obj, Long.parseLong(value));
            } else if (fieldType == float.class || fieldType == Float.class) {
                field.set(obj, Float.parseFloat(value));
            } else if (fieldType == double.class || fieldType == Double.class) {
                field.set(obj, Double.parseDouble(value));
            } else if (fieldType == boolean.class || fieldType == Boolean.class) {
                field.set(obj, Boolean.parseBoolean(value));
            } else {
                // 其他类型暂不处理
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

测试:

//TODO

没有优化:

普通的转化器,但是和JACKSON等JSONUtils区别在于,json出现格式异常后也不报错,仅仅返回匹配成功的部分

import java.lang.reflect.InvocationTargetException;
import java.util.*;

public class JsonConverter {
    public static <T> T convertJsonToEntity(String json, Class<T> clazz, int start, int end) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        T entity = clazz.getDeclaredConstructor().newInstance();

        try {
            if(json.charAt(start) == '{' && json.charAt(end) == '}'){
                start++;
                end--;
                //为了防止原本属于子对象的{或}被异常清理,只能这样;但此时无法处理异常缺失{或者}
            }
            String targetJson = json.substring(start,end);
            targetJson = addGapToEscapeSplitBug(targetJson);
            String[] keyValuePairs = targetJson.split("[,{}]");
            Map<String, Object> map = new HashMap<>();
            int sonIndex = 0;//用于表示{}是否全部抵消
            int sonStart = start;
            int sonEnd = sonStart;
            String sonClass = null;
            Iterator<String> iterator = Arrays.stream(keyValuePairs).iterator();
            boolean inner = false;
            while (iterator.hasNext()) {
                String next = iterator.next();

                if (inner) {
                    if (next.endsWith(":")) {
                        sonIndex++;
                        sonEnd += next.length() + 1 + 1; // 因为{
                        continue;
                    }
                    if (next.length() == 0 || " ".equals(next)) {//1. 出现 }  2. 兼容split异常情况
                        sonIndex--;
//                        sonEnd += 1;//增加 } 的长度 1
                        if (sonIndex == 0) {  //出来了
                            sonEnd--; //由于子对象入口处+2表示将末尾的}算进去了,此时--表示最后一个k-v没有,之前多算了一个
                            inner = false;
                            //对刚刚完成的部分进行子JSON查询
                            Class<?> son = Class.forName(classConverter(sonClass));
                            Object o = convertJsonToEntity(json, son, sonStart, sonEnd);
                            map.put(sonClass.substring(1, sonClass.length() - 2), o);
                            //用于标记子JSON的两个指针对齐
                            sonStart = sonEnd;
                        }
                        continue;
                    }
                    sonEnd += next.length() + 1;
                    continue;
                }
                if (next.endsWith(":")) {
//                    if (sonIndex == 0) {
                    sonStart += next.length() + 1 ;//由于有{ +1
                    sonClass = next;
                    sonEnd = sonStart;
                    inner = true;
//                    }
                    sonIndex++;
                    continue;
                }
                sonStart += next.length() + 1;//加一因为末尾有逗号
                String[] entry = next.split(":");
                map.put(removeQuotationMarkIfExists(entry[0]), removeQuotationMarkIfExists(entry[1]));
            }


            for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                String fieldName = field.getName();
                if (map.containsKey(fieldName)) {
                    Object value = map.get(fieldName);
                    if (value != null) {
                        if (field.getType() == int.class) {
                            field.setInt(entity, Integer.parseInt((String) value));
                        } else {
                            field.set(entity, value);
                        }
                    }
                }
            }

            return entity;
        } catch (Exception e) {
            System.out.println("异常");
            e.printStackTrace();
            return entity;
        }
    }

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        String json = "{"name":"杨得志","age":12,"address":{"name":"123","num":{"big":1,"small":100}},"status":"student"}";
        Student student = convertJsonToEntity(json, Student.class, 0, json.length() - 1);
        System.out.println(student);
//        System.out.println(student.getName());
    }

    public static String classConverter(String origin) {
        StringBuilder sb = new StringBuilder();
        String start = origin.substring(1, 2).toUpperCase();
        String then = origin.substring(2, origin.length() - 2);
        return sb.append(start).append(then).toString();
    }
    public static String removeQuotationMarkIfExists(String origin){
        if(origin.startsWith(""") && origin.endsWith(""")){
            return origin.substring(1,origin.length()-1);
        }else {
            return origin;
        }
    }
    public static String addGapToEscapeSplitBug(String origin){
        if(origin.endsWith("}") && !origin.startsWith("{")){
            return origin + " ";
        }
        return origin;
    }

    static class Student {
        private int id;
        private String name;
        private int age;
        private Address address;

        // Getters and Setters
        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Address getAddress() {
            return address;
        }

        public void setAddress(Address address) {
            this.address = address;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", name='" + name + ''' +
                    ", age=" + age +
                    ", address=" + address +
                    '}';
        }
    }
}