-
之前在生产环境遇到一个问题,日志信息在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 +
'}';
}
}
}