多层嵌套的json的值如何解析/替换
作者:linyb极客之路
前言
前阵子承接了2个需求,一个数据脱敏,一个是低代码国际化多语言需求,这两个需求有个共同特点,都是以json形式返回给前端,而且都存在多层嵌套,其中数据脱敏的数据格式是比较固定,而低代码json的格式存在结构固定和不固定 2种格式。最后不管是数据脱敏或者是多语言,业务抽象后,都存在需要做json值替换的需求。今天就来聊下多层嵌套json值如何解析或者替换
多层嵌套json解析
1、方法一:循环遍历+利用正则进行解析
这种做法相对常规,且解析比较繁琐。
2、方法二:利用OGNL表达式
1、何为OGNL
OGNL(Object-Graph Navigation Language)是一种表达式语言,用于在Java应用程序中对对象图进行导航和操作。OGNL本身并不提供直接的执行环境,它是作为一个库或框架的一部分来使用的。因此,OGNL的执行方式取决于使用它的上下文。
一般情况下,OGNL可以通过两种方式执行:解释执行和编译执行。
解释执行:在解释执行中,OGNL表达式在运行时逐条解释和执行。它会在每次表达式执行时动态计算表达式的结果,并根据对象图的实际状态进行导航和操作。这种方式的灵活性较高,可以根据需要对对象图进行动态操作,但相对而言执行效率较低。
编译执行:为了提高执行效率,有些框架会将OGNL表达式编译成可执行的字节码或类文件。在编译执行中,OGNL表达式在编译阶段被转换成可执行代码,然后在运行时直接执行这些生成的代码。这种方式可以在一定程度上提高执行速度,但牺牲了一些灵活性,因为编译后的代码在运行时不再动态计算。
我们经常使用ORM框架mybatis的动态sql解析,它的实现基石就是OGNL表达式。回到正题,我们如何利用OGNL来解析json
a、 在项目POM引入OGNL GAV
<dependency> <groupId>ognl</groupId> <artifactId>ognl</artifactId> <version>${ognl.version}</version> </dependency>
b、 封装OGNL表达式工具类
public final class OgnlCache { private static final OgnlMemberAccess MEMBER_ACCESS = new OgnlMemberAccess(); private static final OgnlClassResolver CLASS_RESOLVER = new OgnlClassResolver(); private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>(); private OgnlCache() { // Prevent Instantiation of Static Class } public static Object getValue(String expression, Object root) { try { Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null); return Ognl.getValue(parseExpression(expression), context, root); } catch (OgnlException e) { throw new RuntimeException("Error evaluating expression '" + expression + "'. Cause: " + e, e); } } private static Object parseExpression(String expression) throws OgnlException { Object node = expressionCache.get(expression); if (node == null) { node = Ognl.parseExpression(expression); expressionCache.put(expression, node); } return node; } }
c、 封装json工具类
public final class JsonUtil { private JsonUtil(){} public static <T> T parse(String jsonStr, Class<T> clazz) throws Exception { return JSON.parseObject(jsonStr, clazz); } public static Object getValue(Map map, String path) throws Exception { return OgnlCache.getValue(path,map); } }
d、 多层嵌套json解析例子
private void printMenuI18nCodeByOgnl() throws Exception { String menuJson = mockMenuService.getMenuJson(); Map<String, Object> map = JsonUtil.parse(menuJson, Map.class); Object topMenu = JsonUtil.getValue( map,"i18NCode"); Object userMenu = JsonUtil.getValue( map,"children[0].i18NCode"); Object userMenuAdd = JsonUtil.getValue( map,"children[0].children[0].i18NCode"); Object userMenuUpdate = JsonUtil.getValue( map,"children[0].children[1].i18NCode"); Object deptMenu = JsonUtil.getValue( map,"children[1].i18NCode"); Object deptMenuList = JsonUtil.getValue( map,"children[1].children[0].i18NCode"); Object deptMenuDelete = JsonUtil.getValue( map,"children[1].children[1].i18NCode"); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Print MenuI18nCode By Ognl Start <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<,"); System.out.println(topMenu); System.out.println(userMenu); System.out.println(userMenuAdd); System.out.println(userMenuUpdate); System.out.println(deptMenu); System.out.println(deptMenuList); System.out.println(deptMenuDelete); System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Print MenuI18nCode By Ognl End <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<,"); }
注: 示例中的menuJson形如下
{"children":[{"children":[{"children":[],"component":"saas/index","i18NCode":"user.menu.add","id":8,"linkUrl":"/user/add","menuName":"用户新增","parentId":9,"sort":9999},{"children":[],"component":"saas/index","i18NCode":"user.menu.update","id":7,"linkUrl":"/user/update","menuName":"用户编辑","parentId":9,"sort":9999}],"component":"saas/index","i18NCode":"user.menu","id":9,"linkUrl":"/user","menuName":"用户菜单","parentId":1,"sort":9999},{"children":[{"children":[],"component":"saas/index","i18NCode":"dept.menu.list","id":11,"linkUrl":"/dept/list","menuName":"部门列表","parentId":10,"sort":9999},{"children":[],"component":"saas/index","i18NCode":"dept.menu.delete","id":12,"linkUrl":"/dept/delete","menuName":"部门删除","parentId":10,"sort":9999}],"component":"saas/index","i18NCode":"dept.menu","id":10,"linkUrl":"/dept","menuName":"部门菜单","parentId":1,"sort":9999}],"component":"saas/index","i18NCode":"top.menu","id":1,"linkUrl":"/topUrl","menuName":"顶级菜单","parentId":0,"sort":9999}
解析后控制台打印如下
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Print MenuI18nCode By Ognl Start <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<,
top.menu
user.menu
user.menu.add
user.menu.update
dept.menu
dept.menu.list
dept.menu.delete
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Print MenuI18nCode By Ognl End <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<,
OGNL表达式常用例子
1. 基本对象树的访问
对象树的访问就是通过使用点号将对象的引用串联起来进行。
例如:name,department.name,user.department.factory.manager.name
2. 对容器变量的访问
对容器变量的访问,通过#符号加上表达式进行。
例如:#name,#department.name,#user.department.factory.manager.name
3. 使用操作符号
OGNL表达式中能使用的操作符基本跟Java里的操作符一样,除了能使用 +, -, *, /, ++, --, ==, !=, = 等操作符之外,还能使用 mod, in, not in等。
4. 容器、数组、对象
OGNL支持对数组和ArrayList等容器的顺序访问:
例如:group.users[0]
同时,OGNL支持对Map的按键值查找:
例如:#session['mySessionPropKey']
不仅如此,OGNL还支持容器的构造的表达式:
例如:{"green", "red", "blue"}构造一个List,#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}构造一个Map
你也可以通过任意类对象的构造函数进行对象新建:
例如:new java.net.URL("http://localhost/")
5. 对静态方法或变量的访问
要引用类的静态方法和字段,他们的表达方式是一样的@class@member或者@class@method(args):
例如:@com.javaeye.core.Resource@ENABLE,@com.javaeye.core.Resource@getAllResources
6. 方法调用
直接通过类似Java的方法调用方式进行,你甚至可以传递参数:
例如:user.getName(),group.users.size(),group.containsUser(#requestUser)
7. 投影和选择
OGNL支持类似数据库中的投影(projection) 和选择(selection)。
投影就是选出集合中每个元素的相同属性组成新的集合,类似于关系数据库的字段操作。投影操作语法为 collection.{XXX},其中XXX 是这个集合中每个元素的公共属性。
例如:group.userList.{username}将获得某个group中的所有user的name的列表。
选择就是过滤满足selection 条件的集合元素,类似于关系数据库的纪录操作。选择操作的语法为:collection.{X YYY},其中X 是一个选择操作符,后面则是选择用的逻辑表达式。而选择操作符有三种:
? 选择满足条件的所有元素
^ 选择满足条件的第一个元素
$ 选择满足条件的最后一个元素
例如:group.userList.{? #this.name != null}将获得某个group中user的name不为空的user的列表。
多层嵌套json替换
1、方法一:循环遍历+正则进行替换
这种做法相对常规,且替换比较繁琐。
2、方法二:利用json类库,进行替换
以fastJSON为例
a、 在项目pom引入fastJSON GAV
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency>
b、 多层嵌套json替换例子
以将菜单的i18nCode替换为具体语言的值为例
public String reBuildMenuJson(){ String orginalMenuJson = getMenuJson(); JSONObject jsonObject = JSON.parseObject(orginalMenuJson); jsonObject.put(I18N_CODE_COLUMN,mockI18nCache.get(jsonObject.get(I18N_CODE_COLUMN))); reBuildChildJson(jsonObject); return JSON.toJSONString(jsonObject); } private void reBuildChildJson(JSONObject curentObject){ JSONArray children = curentObject.getJSONArray(CHILDREN_COLUMN); for (int i = 0; i < children.size(); i++) { JSONObject child = children.getJSONObject(i); child.put(I18N_CODE_COLUMN,mockI18nCache.get(child.get(I18N_CODE_COLUMN))); reBuildChildJson(child); } }
注: 未替换前,menuJson形如下
{"children":[{"children":[{"children":[],"component":"saas/index","i18NCode":"user.menu.add","id":8,"linkUrl":"/user/add","menuName":"用户新增","parentId":9,"sort":9999},{"children":[],"component":"saas/index","i18NCode":"user.menu.update","id":7,"linkUrl":"/user/update","menuName":"用户编辑","parentId":9,"sort":9999}],"component":"saas/index","i18NCode":"user.menu","id":9,"linkUrl":"/user","menuName":"用户菜单","parentId":1,"sort":9999},{"children":[{"children":[],"component":"saas/index","i18NCode":"dept.menu.list","id":11,"linkUrl":"/dept/list","menuName":"部门列表","parentId":10,"sort":9999},{"children":[],"component":"saas/index","i18NCode":"dept.menu.delete","id":12,"linkUrl":"/dept/delete","menuName":"部门删除","parentId":10,"sort":9999}],"component":"saas/index","i18NCode":"dept.menu","id":10,"linkUrl":"/dept","menuName":"部门菜单","parentId":1,"sort":9999}],"component":"saas/index","i18NCode":"top.menu","id":1,"linkUrl":"/topUrl","menuName":"顶级菜单","parentId":0,"sort":9999}
替换后,menuJson形如下
{"component":"saas/index","children":[{"component":"saas/index","children":[{"component":"saas/index","children":[],"linkUrl":"/user/add","menuName":"用户新增","id":8,"sort":9999,"i18NCode":"userMenuAdd","parentId":9},{"component":"saas/index","children":[],"linkUrl":"/user/update","menuName":"用户编辑","id":7,"sort":9999,"i18NCode":"userUpdateAdd","parentId":9}],"linkUrl":"/user","menuName":"用户菜单","id":9,"sort":9999,"i18NCode":"userMenu","parentId":1},{"component":"saas/index","children":[{"component":"saas/index","children":[],"linkUrl":"/dept/list","menuName":"部门列表","id":11,"sort":9999,"i18NCode":"deptMenuList","parentId":10},{"component":"saas/index","children":[],"linkUrl":"/dept/delete","menuName":"部门删除","id":12,"sort":9999,"i18NCode":"deptMenuDelete","parentId":10}],"linkUrl":"/dept","menuName":"部门菜单","id":10,"sort":9999,"i18NCode":"deptMenu","parentId":1}],"linkUrl":"/topUrl","menuName":"顶级菜单","id":1,"sort":9999,"i18NCode":"topMenu","parentId":0}
3、方法三:利用json序列化注解
以菜单国际化为示例
1、自定义注解
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside @JsonSerialize(using = I18nJsonSerializer.class) public @interface I18nField { }
2、自定义国际化翻译接口(该具体实现留给业务扩展)
public interface I18nService { String getTargetContent(String i18nCode); }
题外话 : 为啥不像spring的messageSource定义成
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
因为很多参数信息可以直接通过上下文获取,比如Locale可以通过LocaleContextHolder.getLocale()
3、编写json序列化接口
public class I18nJsonSerializer extends JsonSerializer<String> implements ContextualSerializer { @Autowired private I18nService i18nService; @Override public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(i18nService.getTargetContent(s)); } @Override public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { I18nField i18nField = beanProperty.getAnnotation(I18nField.class); if(!ObjectUtils.isEmpty(i18nField) && String.class.isAssignableFrom(beanProperty.getType().getRawClass())){ return this; } return serializerProvider.findValueSerializer(beanProperty.getType(),beanProperty); } }
4、定义和json字段能够匹配的对象
大白话,就是json和这个对象可以相互转换。以菜单为例
@Data @EqualsAndHashCode(callSuper = true, of = {"id"}) public class MenuResourceDTO extends TreeDTO<MenuResourceDTO> implements Serializable { private static final long serialVersionUID = 1L; 。。。。。省略其他属性 /** * 单菜名称 */ private String menuName; private String permission; /** * 是否缓存 */ private Integer keepAlive; @I18nField private String i18NCode; public static String I18N_CODE_COLUMN = "i18NCode"; public static String CHILDREN_COLUMN = "children";
5、在需要进行替换的字段上加上 @I18nField注解
@I18nField private String i18NCode;
6、替换验证
编写一个测试controller,用来输出替换后的菜单信息
@RestController @RequestMapping("menu") @RequiredArgsConstructor public class MockMenuController { private final MockMenuService mockMenuService; @GetMapping public MenuResourceDTO getMenu(){ return mockMenuService.getMenuResourceDTO(); } }
通过POSTMAN访问,得到如下信息
{ "id": 1, "parentId": 0, "sort": 9999, "children": [ { "id": 9, "parentId": 1, "sort": 9999, "children": [ { 。。。省略其他信息 "menuName": "用户新增", "i18NCode": "userMenuAdd" }, { 。。。省略其他信息 "menuName": "用户编辑", "i18NCode": "userUpdateAdd" } ], 。。。省略其他信息 "menuName": "用户菜单", "i18NCode": "userMenu" }, "menuName": "顶级菜单", "i18NCode": "topMenu" }
回答上面多层json解析的方法三,那个悬念做法就是将json与对象映射起来,通过对象来取值
总结
本文的多层嵌套json的解析和替换都提供了几种方案,综合来讲是推荐将json先转对象,通过对象操作。对json替换,推荐使用自定义json序列化注解的方式。但这种方式比较适合json的结构以及字段是固定的方式。对于低代码,本身的json结构是多种多样的,如果要后端实现,一种做法,就是将这些json都映射成对象,但因为json结构多种多样,就会导致要映射的对象膨胀。另一种方式,是直接转JsonObject,通过JsonObject来操作替换
其次现在都是前后端分离,有些东西其实也可以放在前端实现,比如这种替换工作其实挺适合放在前端做的。以低代码为例,因为前端本来就需要解析json,后端可以维护一个映射表,前端实现一个组件函数,通过该函数优先从前端缓存取,取不到再从调用后端接口,这就是json替换的方法四,把替换工作留给前端做,哈哈。大家是一个团队,哪边好实现,就放哪边做
最后那个ognl的代码,我是直接把mybatis的源码搬过来,直接套用了。开源有的东西,就没必要自己再搞一遍了
以上就是多层嵌套的json的值如何解析/替换的详细内容,更多关于多层嵌套json值解析替换的资料请关注脚本之家其它相关文章!