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 {
// 测试demo
public static void main(String[] args) {
String testJson = ".........."; // 省略json
try {
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=贰佰圆整)