最近在做物流供应链相关项目的时候,遇到了一个需求,某维度下会有各种金额的属性,而金额会有多种,需要动态获取,而我们知道,java后端返回的VO是需要预先定义出来的,并且项目中的国际化方案也是需要对应明确的字段名称,那么要如何设计并解决这个问题呢。
下面的代码都不是真实的项目代码,但是道理是一样的。
略去基础springboot项目的搭建,直接到具体的代码。
1、首先,原来的对象中,肯定还是要接收这些对象,那么可以想到-Map对象可以存放一个映射,可以用来在源对象存放扩展字段,用于后续动态生成字段并返回。
原对象:
package com.example.demo.model;
import lombok.Data;
import java.util.Map;
/**
* 原对象
*/
@Data
public class User {
private String userName;
private String pwd;
//扩展字段
private Map<String, String> extendFields;
}
controller:
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.util.RestResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class TestDynamicClassController {
@GetMapping("/testDynamic")
public RestResponse testDynamic() {
User user = new User();
user.setUserName("xxx");
user.setPwd("pwd");
Map<String, String> map = new HashMap<>();
map.put("extendField1", "extendField1");
map.put("extendField2", "extendField2");
user.setExtendFields(map);
return new RestResponse(0, "success", user);
}
}
此时返回内容:
GET http://localhost:443/testDynamic
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 26 Mar 2022 07:26:11 GMT
{
"code": 0,
"message": "success",
"data": {
"userName": "xxx",
"pwd": "pwd",
"extendFields": {
"extendField2": "extendField2",
"extendField1": "extendField1"
}
}
}
Response code: 200; Time: 203ms; Content length: 145 bytes
此时返回的对象中扩展字段为map形式,并不能达到和userName等字段平级的效果
2、根据原对象,处理动态生成对象,去掉map,变成平级的字段形式
2.1、引入动态修改的类
ReflectUtil:
package com.example.demo.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.springframework.cglib.beans.BeanGenerator;
import org.springframework.cglib.beans.BeanMap;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
/**
* 动态生成工具类
*/
@Slf4j
public class ReflectUtil {
public static Object getObject(Object dest, Map<String, Object> newValueMap) throws InvocationTargetException, IllegalAccessException {
PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean();
//1.获取原对象的字段数组
PropertyDescriptor[] descriptorArr = propertyUtilsBean.getPropertyDescriptors(dest);
//2.遍历原对象的字段数组,并将其封装到Map
Map<String, Class> oldKeyMap = new HashMap<>();
for (PropertyDescriptor it : descriptorArr) {
if ("extendFields".equalsIgnoreCase(it.getName())) {
continue;
}
oldKeyMap.put(it.getName(), it.getPropertyType());
newValueMap.put(it.getName(), it.getReadMethod().invoke(dest));
}
//3.将扩展字段Map合并到原字段Map中
newValueMap.forEach((k, v) -> oldKeyMap.put(k, v.getClass()));
//4.根据新的字段组合生成子类对象
DynamicBean dynamicBean = new DynamicBean(dest.getClass(), oldKeyMap);
//5.放回合并后的属性集合
newValueMap.forEach((k, v) -> {
try {
dynamicBean.setValue(k, v);
} catch (Exception e) {
log.error("动态添加字段【值】出错", e);
}
});
return dynamicBean.getTarget();
}
}
class DynamicBean {
private Object target;
private BeanMap beanMap;
public DynamicBean(Class superclass, Map<String, Class> propertyMap) {
this.target = generateBean(superclass, propertyMap);
this.beanMap = BeanMap.create(this.target);
}
public void setValue(String property, Object value) {
beanMap.put(property, value);
}
public Object getValue(String property) {
return beanMap.get(property);
}
public Object getTarget() {
return this.target;
}
/**
* 根据属性生成对象
*/
private Object generateBean(Class superclass, Map<String, Class> propertyMap) {
BeanGenerator generator = new BeanGenerator();
if (null != superclass) {
generator.setSuperclass(superclass);
}
BeanGenerator.addProperties(generator, propertyMap);
return generator.create();
}
}
修改后的对象:
package com.example.demo.model;
import lombok.Data;
/**
* 原对象
*/
@Data
public class User {
private String userName;
private String pwd;
}
修改后的controller:
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.util.ReflectUtil;
import com.example.demo.util.RestResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
@RestController
public class TestDynamicClassController {
private static final ObjectMapper MAPPER = new ObjectMapper();
@GetMapping("/testDynamic")
public RestResponse<Object> testDynamic() throws JsonProcessingException, InvocationTargetException, IllegalAccessException {
User user = new User();
user.setUserName("xxx");
user.setPwd("pwd");
Map<String, Object> map = new HashMap<>();
map.put("extendField1", "extendField1");
map.put("extendField2", "extendField2");
System.out.println("User:" + MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(user));
//处理动态字段
Object obj = ReflectUtil.getObject(user, map);
System.out.println("User:" + MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj));
return new RestResponse(0, "success", obj);
}
}
res:
GET http://localhost:443/testDynamic
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 26 Mar 2022 08:07:35 GMT
{
"code": 0,
"message": "success",
"data": {
"userName": "xxx",
"pwd": "pwd",
"extendField2": "extendField2",
"extendField1": "extendField1"
}
}
Response code: 200; Time: 1798ms; Content length: 128 bytes
可以看到,在返回值中,动态增加了两个列
缺点:返回对象为object对象,只是返回值内容符合了要求,无法对代码做更多的操作(毕竟它不是一个明确的对象,没有明确的字段)。
======================================
2022.6.22 对之前最终的方案做一个回顾
这个方案太重了,每次需要新加载这个新生成的类,而且无法拿到扩展字段去做进一步处理,不够灵活,最后没有用这种方式去实现;
最终方案是增加一个扩展字段,ex:
{
"fulfillNo": "17183",
"bizNo": "TEST999",
"xxx": "xxx",
...
"extendInfo": {
"ROAD_0081": "",
"ROAD_0082": "",
"ROAD_0062": "",
"WY_ROAD_0007": "",
...
}
}
其实之前用动态生成字段的方式有一个考虑是前端表头获取字段的形式是jsonStr.fieldName,钻了牛角尖,经小伙伴提醒:其实扩展字段新增一层也没有问题,只要和前端沟通,规定这个扩展字段的名称后,以jsonStr.extendInfo.field的形式去取即可。
这样不用反射,避免降低程序执行的效率,也降低了程序的复杂度。
总结:做需求要灵活,多考虑方案,多沟通。