PropertyUtilsBean

在实际的工程设计中,当我们设计了一个复杂的数据对象,对象中还嵌套有子对象,子对象可能还会有更多的嵌套时,如果没有工具辅助,要获取一个对象的子成员下的子成员,需要写好几行代码逐级获取,这中间还要涉及到判空的问题,如果成员类型是Map/JSON对象那还要从Map中读取子成员,如果是Sting 类型JSON字符串,那获取下面的子成员更麻烦还要涉及解析JSON解析。往涉及到这种复杂的多级嵌套的子成员变量读写,程序代码都会变得很臃肿,繁琐。

apache 的开源库 common-beanutils 中的 org.apache.commons.beanutils.PropertyUtilsBean 类就是为了解决这个问题而设计的,它实现了 Java Bean 的多级嵌套读写访问。

为了实现对复杂数据对象子成员的读写,需要支持嵌套的多级字段名表达式定义的字段名。为实现此目标,PropertyUtilsBean定义了五种引用 bean 的特定属性值的格式,如下,括号中是标识字符串的默认格式。这些格式的符号以及它们的解析方式由的Resolver实现:

  • 【简单格式】
    Simple ( name) – 指定 name标识特定 JavaBean 的单个属性。要使用的实际 getter 或 setter 方法的名称是 JavaBeans 标准定义的,例如,名为“xyz”的属性将有一个名为getXyz()isXyz()(仅用于布尔属性)的 getter 方法,以及setter 方法名为 setXyz().
  • 【嵌套格式】
    Nested ( name1.name2.name3) --第一个 name 元素用于选择一个属性 getter,就像上面的简单引用一样。然后使用相同的方法查询为此属性返回的对象,以获取名为 的属性的属性获取器name2,依此类推。最终检索或修改的属性值是由最后一个名字节点元素标识的值。
  • 【索引格式】
    Indexed ( name[index]) – 假定属性值是一个数组或列表,或者假定此 JavaBean 具有索引属性 getter 和 setter 方法。定位数组/列表中(based-0)索引指定的值。
  • 【MAP格式】
    Mapped( name(key)) – 假定 JavaBean 有一个属性 getter 和 setter 方法以及一个额外的 type 属性java.lang.String。适用于Map。
  • 【组合格式】
    Combined( name1.name2[index].name3(key)) - 支持上述4种表达的组合。

PropertyUtilsBean这么设计看起挺全面的,然而在实际工程应用中,发现这与现实是拖节的,并不好用,遇到如下问题:

  • 上述嵌套组合中,中间的任意一个节点为null时PropertyUtilsBean只会简单的抛出异常。
  • 如果有String类型的JSON字段,并不支持JSON中的成员的读取或写入,现在JSON在工程应用中被广泛使用,不支持JSON字符串访问,会大大限制其使用范围。
  • 对于通过索引格式(Indexed)访问数组或列表,如果下标越界只会简单抛出异常。
  • 不支持向列表中添加元素。
  • 对于数组和列表只能通过索引访问,不支持在数组或列表中通过简单的字段名匹配查找元素。

在使用PropertyUtilsBean过程中遇到如上种种问题导致我的工作不得停顿下来,所以下决心做一个趁手的工具来实现我的需要。

BeanPropertySupport

BeanPropertySupport是参照apache 的开源库 common-beanutils 中的 org.apache.commons.beanutils.PropertyUtilsBean 类实现 Java Bean 的多级嵌套读写工具类,相比PropertyUtilsBeanBeanPropertySupport增加、扩展了如下特性:

  • 支持String类型的JSON (需要JSON库[fastjson 或jackson]支持) 的字段内成员读写。
  • 写操作支持自动尝试创建成员对象,即当要访问的嵌套字段名 name1.name2.name3中任何一个中间节点为null时会尝试创建一个空的节点以最大限度能让节点遍历进行下去。Map,List,有默认构造方法或复制构造方法的类型都支持自动创建成员。
  • 嵌套字段名表达式在.【简单成员】,[]【数组列表索引】,()【Map】的基础上增加了[k=v]【搜索】—数组/列表中按字段名条件搜索。如users[name=tom]即在数组或列表中代表字段nametom的第一个元素。
  • 索引表达式[]支持扩展表达[+],[-],[FIRST],[LAST],用于支持在列表头尾添加元素,或获取列表/数组的头尾部元素。
  • 增加different方法用于返回两个对象的字段值差异详细描述。
  • 读取操作如果名字节点中任意一个节点的值为null则返回null,不会抛出异常。

索引扩展表达式

索引表达式[]支持扩展表达[+],[-],[FIRST],[LAST],用于支持在列表头尾添加元素,或获取列表/数组的头尾部元素。

读取或写入时的表达式说明:

表达式

适用

说明

[-],[-1],[FIRST]

读取

数组/列表第一个元素

[+],[-2],[LAST]

读取

数组/列表最后一个元素

[-],[-1],[FIRST]

写入

列表头部添加一个元素[不支持数组]

[+],[-2],[LAST]

写入

列表尾部添加一个元素[不支持数组]

索引扩展表达式[+]示例:

@Test 
	public void test10Index(){
    	PublicFieldBean bean = new PublicFieldBean("tom","guangzhou",23,null,null);
    	/**  添加到列表尾部测试 */
		JSONObject cherry = new JSONObject().fluentPut("name", "cherry").fluentPut("phone", 10090125622L).fluentPut("country", "gm");
    	BEAN_SUPPORT.setProperty(bean, "props.users[+]", cherry);
    	assertTrue("cherry".equals(BEAN_SUPPORT.getProperty(bean, "props.users[+].name")));
	}

JSON 字段读写

BeanPropertySupport支持String类型的JSON (需要JSON库[fastjson or jackson]支持) 的字段内成员读写.示例如下:

@Test
    public void test6JsonString(){
    	String json = "{\"modified\":0,\"initialized\":8388607,\"new\":false,\"id\":3,\"groupId\":2,\"features\":0,\"name\":\"hello5\",\"physicalAddress\":\"000000000002\",\"addressType\":\"MAC\",\"iotCard\":null,\"status\":\"ENABLE\",\"tokenTime\":0,\"screenInfo\":\"21V960x1080\",\"fixedMode\":\"FLOOR\",\"osArch\":null,\"network\":\"4G\",\"versionInfo\":null,\"model\":\"EAMDEV0\",\"vendor\":null,\"deviceDetail\":{\"device_name\":\"AN01\",\"manufacturer\":\"NXP\",\"made_date\":\"2022-01-02\"},\"props\":{\"disk_capacity\":\"1.2GB\"},\"planId\":\"3709047235ABCEDFG\",\"targetId\":\"20220825182312501665d\",\"remark\":null,\"updateTime\":\"2022-09-01 17:49:22\",\"createTime\":\"2022-08-03 12:21:38\"}";
    	PublicFieldBean bean = new PublicFieldBean("tom","guangzhou",23,null,null);
        /** props 为 String类型的JSON 字段 */
    	bean.setJsonProps(json);
    	try {
    		/** String类型JSON 字段测试 */
			assertTrue(BEAN_SUPPORT.getPropertyChecked(json, "id").equals(3));
			assertTrue(BEAN_SUPPORT.getPropertyChecked(json, "props.disk_capacity").equals("1.2GB"));
			BEAN_SUPPORT.setPropertyChecked(bean, "jsonProps.props.remark", "hello");
			assertTrue("hello".equals(BEAN_SUPPORT.getPropertyChecked(bean, "jsonProps.props.remark")));
			/** JSON字符串为输入参数测试,这种情况下要从返回值获取修改后的字符串 */
			String json2 = (String)BEAN_SUPPORT.setPropertyChecked(json, "props.remark", "hello");
			assertTrue("hello".equals(BEAN_SUPPORT.getPropertyChecked(json2, "props.remark")));
			/** JSON String字段初始为null的读写测试 */
			bean.setJsonProps(null);
			assertNull(BEAN_SUPPORT.getPropertyChecked(bean, "jsonProps.remark"));
			BEAN_SUPPORT.setPropertyChecked(bean, "jsonProps.props.remark", "hello");
			assertTrue("hello".equals(BEAN_SUPPORT.getPropertyChecked(bean, "jsonProps.props.remark")));
			/** JSON String字段初始为null的直接写入JSON对象 */
			bean.setJsonProps(null);
			JSONObject newjson=new JSONObject().fluentPut("name", "jerry").fluentPut("address", "拉萨路小学").fluentPut("updateTime", "2003-01-01 00:00:00");
			BEAN_SUPPORT.setPropertyChecked(bean, "jsonProps", newjson);
			assertTrue("jerry".equals(BEAN_SUPPORT.getPropertyChecked(bean, "jsonProps.name")));
			/** 向已经有的JSON string中更新内容 */
			String jstr="{\"targetId\":\"20220825182312501665d\",\"remark\":null,\"updateTime\":\"2022-09-01 17:49:22\",\"createTime\":\"2022-08-03 12:21:38\"}";
			BEAN_SUPPORT.setPropertyChecked(bean, "jsonProps", jstr);
			assertTrue("jerry".equals(BEAN_SUPPORT.getPropertyChecked(bean, "jsonProps.name")));
			assertTrue("2022-09-01 17:49:22".equals(BEAN_SUPPORT.getPropertyChecked(bean, "jsonProps.updateTime")));
			assertTrue("20220825182312501665d".equals(BEAN_SUPPORT.getPropertyChecked(bean, "jsonProps.targetId")));			
		} catch (Exception e) {
			e.printStackTrace();
			assertTrue(false);
		}
    }

BeanProperySupport默认使用fastjson或jackson来实现JSON的序列化和反序列化,你需要在自己的项目中添加fastjson或jackson的依赖

fastjson dependency

<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.83</version>
		</dependency>

jackson dependency

<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
            <version>2.8.10</version>
	    </dependency>

你也可以基于其他JSON工具继承net.gdface.json.JsonSupport类实现自定义的JSON解析类,并调用 JsonSupports.setJsonSupportInstance(JsonSupport instance) 方法指定使用自己的JsonSupport对象.

Searched表达式

BeanPropertySupport在索引表达式的基础上增加了增加了[k=v]字段搜索表达式支持在对象数组列表中根据通过字段匹配的值的条件查找第一个元素,示例如下:

/**
     * 测试数组搜索表达式 [key=value]
     */
    @Test
    public void test7SearchExpress(){
    	PublicFieldBean bean = new PublicFieldBean("tom","guangzhou",23,null,null);
    	ArrayList<JSONObject> list = Lists.newArrayList(
    			new JSONObject().fluentPut("name", "jerry").fluentPut("phone", 13077988845L).fluentPut("country", "usa"),
    			new JSONObject().fluentPut("name", "sam").fluentPut("phone", 13082171823L).fluentPut("country", "uk"),
    			new JSONObject().fluentPut("name", "lang").fluentPut("phone", 15022983884L).fluentPut("country", "cn"),
    			new JSONObject().fluentPut("name", "brown").fluentPut("phone", 17700261845L).fluentPut("country", "hk")
    			);
    	bean.setLogs(list);
    	
    	try {
    		/** 在 logs 数组中搜索name字段为jerry的对象 */
			Object element = BEAN_SUPPORT.getPropertyChecked(bean, "logs[name=jerry]");
			assertNotNull(element);
			assertTrue("usa".equals(BEAN_SUPPORT.getPropertyChecked(element, "country")));
			BEAN_SUPPORT.setPropertyChecked(bean, "logs[name=jerry].phone",16887822235L);
			element = BEAN_SUPPORT.getPropertyChecked(bean, "logs[name=jerry]");
			assertTrue(Long.valueOf(16887822235L).equals(BEAN_SUPPORT.getPropertyChecked(element, "phone")));
			
		} catch (Exception e) {
			e.printStackTrace();
			assertTrue(false);
		}
    }

different 记录字段差异

different是BeanPropertySupport增加的一个功能,即对两个对象进行比较(可以是不同类型)逐字段返回,不同的字段的差异,示例如下:

@Test
    public void test8Different(){
    	try {
        	String json1 = "{\"modified\":0,\"initialized\":8388607,\"new\":false,\"id\":3,\"groupId\":2,\"features\":0,\"name\":\"hello5\",\"physicalAddress\":\"000000000002\",\"addressType\":\"MAC\",\"iotCard\":null,\"status\":\"ENABLE\",\"tokenTime\":0,\"screenInfo\":\"21V960x1080\",\"fixedMode\":\"FLOOR\",\"osArch\":null,\"network\":\"4G\",\"versionInfo\":null,\"model\":\"EAMDEV0\",\"vendor\":null,\"deviceDetail\":{\"device_name\":\"AN01\",\"manufacturer\":\"NXP\",\"made_date\":\"2022-01-02\"},\"props\":{\"disk_capacity\":\"1.2GB\"},\"planId\":\"3709047235ABCEDFG\",\"targetId\":\"20220825182312501665d\",\"remark\":null,\"updateTime\":\"2022-09-01 17:49:22\",\"createTime\":\"2022-08-03 12:21:38\"}";
        	String json2 = "{\"modified\":0,\"initialized\":8388607,\"new\":true,\"id\":22,\"groupId\":2,\"features\":0,\"name\":\"hello5\",\"physicalAddress\":\"000000000002\",\"addressType\":\"MAC\",\"iotCard\":null,\"status\":\"ENABLE\",\"tokenTime\":0,\"screenInfo\":\"21V960x1080\",\"fixedMode\":\"FLOOR\",\"osArch\":null,\"network\":\"4G\",\"versionInfo\":null,\"model\":\"EAMDEV0\",\"vendor\":null,\"deviceDetail\":{\"device_name\":\"AN01\",\"manufacturer\":\"NXP\",\"made_date\":\"2022-01-02\"},\"props\":{\"disk_capacity\":\"1.2GB\"},\"planId\":\"3709047235ABCEDFG\",\"targetId\":\"20220825182312501665d\",\"remark\":null,\"updateTime\":\"2022-09-01 17:49:22\",\"createTime\":\"2022-08-03 12:21:38\"}";

    		PublicFieldBean bean1 = new PublicFieldBean("tom","guangzhou",23,new Date(),null);
    		bean1.setJsonProps(json1);
    		StandardBean standardBean = new StandardBean("070199", "北京路32号")
    				.setProps(new JSONObject().fluentPut("last_date", "1973-01-01"));
    		@SuppressWarnings("deprecation")
			PublicFieldBean bean2 = new PublicFieldBean("jerry","shanghai",7,new Date(103,1,1),standardBean);
    		bean2.setJsonProps(json2);
    		Map<String, DiffNode> diffNodes = BEAN_SUPPORT.different(bean1, bean2);
    		log("diff Nodes \n{}",jsonSupportInstance().toJSONString(diffNodes,true));
		} catch (Exception e) {
			e.printStackTrace();
			assertTrue(false);
		}
    }

以上调用返回差异结果如下,left即为左边对象对应的字段值,right为右侧对象对应的字段值:

{
	"createTime":{
		"left":"2022-11-09 23:45:44",
		"right":"2003-02-01 00:00:00"
	},
	"groupId":{
		"left":23,
		"right":7
	},
	"name":{
		"left":"tom",
		"right":"jerry"
	},
	"jsonProps.id":{
		"left":3,
		"right":22
	},
	"jsonProps.new":{
		"left":false,
		"right":true
	},
	"location":{
		"left":"guangzhou",
		"right":"shanghai"
	},
	"child":{
		"left":"null",
		"right":{
			"address":"北京路32号",
			"historyNumer":"070199",
			"props":{
				"last_date":"1973-01-01"
			}
		}
	}
}

Android支持

因为android虚拟机缺少java.beans包所以androi平台支持需要额外依赖库支持:

implementation group: 'me.champeau.openbeans', name: 'openbeans', version: '1.0.2'

implementation group: 'com.googlecode', name: 'openbeans', version: '1.0'

源码/dependency

BeanPropertySupport 的完整代码参见码云仓库:

https://gitee.com/l0km/common-java/tree/master/common-base2/src/main/java/net/gdface/bean

BeanPropertySupport 的maven引用:

<dependency>
			<groupId>com.gitee.l0km</groupId>
			<artifactId>common-base2</artifactId>
			<version>2.7.5</version>
		</dependency>