Java反射实战-特殊嵌套格式JSON自定义解析装配
问题描述
业务需要调用一个第三方图像识别的API,然后将识别出来的结果区分出不同类型,并分别装配到对应的Java实体类里
听起来是一个很正常的需求,现在JSON解析的工具有很多,而且都很成熟,但奈何识别出的JSON结果嵌套了很多复杂的结构,找不到现成的工具方法解决这个问题,JSON格式如下所示:
{"words_result": [{"result": {"AmountInWords": [{"word": "贰佰圆整"}],"InvoiceNumConfirm": [{"word": "8xxxxx13"}],// ================================省略n行// ****************************************"CommodityPrice": [{"row": "1","word": "7.1238475"}],"CommodityNum": [{"row": "1","word": "24.8447205"}],"CommodityTaxRate": [{"row": "1","word": "13%"}],"InvoiceCode": [{"word": "0xxxxx611"}],"AmountInFiguers": [{"word": "200.00"}],"CommodityAmount": [{"row": "1","word": "176.99"}],"CommodityType": [{"row": "1","word": "92号"}],"CommodityTax": [{"row": "1","word": "23.01"}],"CommodityUnit": [{"row": "1","word": "升"}],"CommodityName": [{"row": "1","word": "*汽油*92号车用汽油(VIB)"}],"InvoiceNum": [{"word": "8xxxxx3"}]},"top": 0,"left": 0,"probability": 0.9595113397,"width": 845,"type": "vat_invoice","height": 544}],"words_result_num": 1,"pdf_file_size": 1,"log_id": "1xxxxxxxxxxxx8"
}
因此我需要手动设计JSON解析的方法
问题分析
因为需要根据不同的识别结果种类,装配到不同的实体类中,并且可能会灵活增加或删除种类,所以需要设计一套易于拓展,方便开发的的方案
根据这样的业务场景,我决定使用工厂模式的策略模式来实现,创建一个总的接口类,子类都实现该接口类,并且使用Java的反射机制,解析实体类的属性,和JSON识别结果的参数名进行一一查找对应,然后可以使用注解来标注类属性名和参数名的映射关系
这样可以实现通用的,易于拓展的代码,需要改变参数值或增加减少识别种类,只需要改变实体类属性本身,不需要改变装配代码
(原JSON中参数名下带"row"字段的,需要额外放到实体类下货品类数组中,row代表第几行的index,和实体类本身是父子关系)
问题解决
接口枚举类样例
// 接口类
public interface Invoice {String getInvoiceNum();String getAmount();
}// 字段映射注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InvoiceJsonField {String value();
}// 识别种类枚举
public enum InvoiceTypeEnum {VAT_INVOICE("vat_invoice"),TICKET_INVOICE("ticket_invoice");private String typeCode;InvoiceTypeEnum(String typeCode) {this.typeCode = typeCode;}public String getTypeCode() {return typeCode;}public void setTypeCode(String typeCode) {this.typeCode = typeCode;}
}
实体类样例
// 实体类种类1-实现接口Invoice
@Data
public class VatInvoice implements Invoice {private String invoiceNum;private String invoiceCode;private List<CommodityInfo> commodityList; // 货品list// 属性名与参数名不一致,同注解写明映射关系@InvoiceJsonField("AmountInFiguers")private String amount;private String amountInWords;
}// 实体类种类2-实现接口Invoice
@Data
public class TicketInvoice implements Invoice {@InvoiceJsonField("ticketNum")private String invoiceNum;private String invoiceName;@InvoiceJsonField("AmountInFiguers")private String ticketAmount;private String amountInWords;
}// 货品类,JSON中带"row"字段的属性,row代表第几行的index
@Data
public class CommodityInfo implements Serializable {@InvoiceJsonField("commodityName")private String name; //货物名称@InvoiceJsonField("commodityType")private String type;//规格型号@InvoiceJsonField("commodityUnit")private String unit;//单位@InvoiceJsonField("commodityNum")private String num;//数量@InvoiceJsonField("commodityPrice")private String price;//单价@InvoiceJsonField("commodityAmount")private String amount;//金额@InvoiceJsonField("commodityTaxRate")private String taxRate;//税率@InvoiceJsonField("commodityTax")private String tax;//税额
}
工厂类
parseInvoice() 方法构造目标对象
public class InvoiceFactory {public static Invoice parseInvoice(JSONObject resObject) throws JsonProcessingException, IllegalAccessException {ObjectMapper objectMapper = new ObjectMapper();JsonNode rootNode = objectMapper.readTree(resObject.toString());String type = rootNode.get("words_result").get(0).get("type").asText();JsonNode wordsResultNode = rootNode.get("words_result").get(0).get("result");Invoice invoice;Boolean isCommodity = false;if (type.equals(InvoiceTypeEnum.VAT_INVOICE.getTypeCode())){invoice = new VatInvoice();isCommodity = true;}else if (type.equals(InvoiceTypeEnum.TICKET_INVOICE.getTypeCode())){invoice = new TicketInvoice();}else{//===加新的类只需要在这加,类实现Invoice接口,添加枚举种类,类属性名和接口返回值属性名一致,驼峰命名首字母小写============throw new RuntimeException(); // 可改为自定义异常抛错}return parseInvoice(invoice, wordsResultNode,isCommodity);}private static Invoice parseInvoice(Invoice invoice, JsonNode wordsResultNode, Boolean isCommodity) throws IllegalAccessException {List<CommodityInfo> infoList = new ArrayList<>();// 反射机制-解析类Class<?> clazz = invoice.getClass();Field[] fields = clazz.getDeclaredFields();Iterator<Map.Entry<String, JsonNode>> fieldsIterator = wordsResultNode.fields();while (fieldsIterator.hasNext()) {// 遍历参数与实体类Map.Entry<String, JsonNode> jsonField = fieldsIterator.next();String jsonKey = jsonField.getKey();JsonNode jsonValue = jsonField.getValue();String javaFieldName = toLowerCaseCamelCase(jsonKey);if (isCommodity){// 装配List<CommodityInfo>setCommodity(infoList,javaFieldName,jsonValue);}// 装配一般字段setFieldValueFor(fields, invoice, jsonValue, javaFieldName);}if (isCommodity){for (Field field : fields) {if (field.getName().equals("commodityList")){field.setAccessible(true);field.set(invoice,infoList);}}}return invoice;}// 装配List<CommodityInfo>private static void setCommodity(List<CommodityInfo> commodity,String javaFieldName,JsonNode jsonValue){JsonNode jsonNode = jsonValue.get(0);if(jsonNode != null && jsonNode.get("row") != null){Integer index = jsonNode.get("row").asInt() - 1;if (commodity.size()<index+1){CommodityInfo commodityInfo = new CommodityInfo();setFieldValueFor(CommodityInfo.class.getDeclaredFields(),commodityInfo,jsonNode,javaFieldName);commodity.add(index,commodityInfo);}else{setFieldValueFor(CommodityInfo.class.getDeclaredFields(),commodity.get(index),jsonNode,javaFieldName);}}}// 属性名驼峰转换private static String toLowerCaseCamelCase(String str) {if (str == null || str.isEmpty()) {return str;}StringBuilder result = new StringBuilder();boolean capitalizeNext = false;for (char c : str.toCharArray()) {if (c == '_') {capitalizeNext = true;} else {if (capitalizeNext) {result.append(Character.toUpperCase(c));capitalizeNext = false;} else {result.append(c);}}}str = result.toString();return Character.toLowerCase(str.charAt(0)) + str.substring(1);}// 反射机制给实体类set值private static void setFieldValueFor(Field[] fields, Object object, JsonNode jsonValue, String jsonFieldName) {for (Field field : fields) {InvoiceJsonField annotation = field.getAnnotation(InvoiceJsonField.class);// 如果有注解映射关系,优先寻找,没有则默认按属性名走boolean shouldMap = (annotation != null && toLowerCaseCamelCase(annotation.value()).equals(jsonFieldName)) ||(annotation == null && field.getName().equals(jsonFieldName));if (shouldMap) {try {// 设置字段值Object value = parseFieldValue(field.getType(), jsonValue);field.setAccessible(true);if (value.getClass().isAssignableFrom(String.class)){field.set(object, value);}else{field.set(object, ((TextNode)value).textValue());}} catch (IllegalAccessException e) {e.printStackTrace();}}}}// 设置字段值private static Object parseFieldValue(Class<?> fieldType, JsonNode jsonValue) {if (jsonValue == null || jsonValue.isNull()) {return null;}// 去掉word这一层级,手动放值if (fieldType.isArray()) {ArrayNode arrayNode = (ArrayNode) jsonValue;List<String> list = new ArrayList<>();for (JsonNode item : arrayNode) {list.add(String.valueOf(item.get("word").asText()));}return list.toArray(new String[0]);}if (fieldType.isAssignableFrom(String.class)) {if (jsonValue.get("word")!=null){return jsonValue.get("word").asText();}else{ArrayNode arrayNode = (ArrayNode) jsonValue;String builder = "";if (arrayNode.isEmpty()){return "";}else{builder = builder + arrayNode.get(0).get("word").asText();for (int i = 1; i < arrayNode.size(); i++) {builder = builder + ",";builder = builder + arrayNode.get(i).get("word").asText();}}return builder.toString();}}return null;}}
结果测试
对于问题描述中给出的识别JSON样例,进行解析测试
public class Main {// 测试demopublic static void main(String[] args) {String testJson = ".........."; // 省略jsontry {Invoice invoice = InvoiceFactory.parseInvoice(JSONObject.parseObject(testJson));System.out.println(invoice);} catch (JsonProcessingException e) {throw new RuntimeException(e);} catch (IllegalAccessException e) {throw new RuntimeException(e);}}
}
测试打印结果,可以看到属性值都已经正确放到实体类中了
VatInvoice(invoiceNum=8xxxxx3, invoiceCode=0xxxxx611, commodityList=[CommodityInfo(name=*汽油*92号车用汽油(VIB), type=92号, unit=升, num=24.8447205, price=7.1238475, amount=176.99, taxRate=13%, tax=23.01)], amount=200.00, amountInWords=贰佰圆整)