一、业务背景介绍

笔者负责了转转APP后端研发工作,主要的模块有首页、列表、详情页、个人中心等。在负责的详情页模块中,有这样的一个场景,APP打开不同商品的时候,会根据商品所属的业务,跳转到对应业务所在的M页。APP原生页面仅支持最基础样式的详情页,其他业务均为M页实现,平台承担着一个路由转发的基础功能。

二、工程现状

重构前配置方式采用的是Map,Key为拼接的属性条件,Value为跳转的配置实体。

重构前的配置示意:

{"label_红布林标签ID": {"url": "红布林落地页地址",   "name": "红布林配置","其他字段": "省略表示"  },"uid_帮卖用户ID_searchType_2": {"url": "转转帮卖暗拍落地页地址",   "name": "转转帮卖暗拍配置",},"uid_帮卖用户ID": {"url": "转转帮卖落地页地址",   "name": "转转帮卖配置",},"cateId_图书社会科学分类ID": {"url": "图书科学类落地页地址","name": "图书科学类配置",},"cateId_图书自然科学分类ID": {"url": "图书科学类落地页地址", "name": "图书科学类配置",}
}

匹配流程示意:

接手此模块的开发工作后,这种开发模式随着需求的迭代暴露出以下问题:

缺乏动态扩展性:

  • 每次新增一种配置维度,需要上线才能完成Key的拼接逻辑和读取逻辑,且读取的优先级硬编码。
  • 已有的属性无法自由组合,如转转帮卖暗拍配置的规则为:卖家ID+搜索类型叠加,只能硬编码的方式进行拼接Key,
    下次的组合维度发生变化还需要开发。

臃肿配置不易维护:

  • 相同业务需要维护多条配置,如图书社会科学和自然科学对应的落地页地址都是一个,按照Map的的配置方式需要配置2条、3条、甚至更多。
  • 长此以往,Key的拼接和解析逻辑越来越难以维护,配置也越来越庞大。

投入产出不成正比:

  • 每次上线只是开发拼接Key的逻辑和读取逻辑,都是重复工作,成本高,收益低。

通过以上问题分析,我们理想的解决方案应该具备高扩展、低成本、易维护的特点。新需求做到尽可能不上线,通过简单的配置,比如用商品中的属性作为动态配置进行逻辑运算, 即可高效率响应业务多变的跳转需求

三、方案选型

方案的大体思路把变化的条件和判断通过配置来实现,自然回想到了熟悉的JSP中的EL表达式求值,利用EL表达式可以求出对象中的任意属性值,然后再对这些求值做逻辑运算,得到True和False就可以了。那么如何实现这个求值和逻辑运算呢,我们选取了几种主表达式引擎作对比如下:

SpringEl Aviator MVEL
简介 Spring体系 开源 开源
优点 在Spring框架中集成度高,属性求值方便 轻量级,高性能 动态JIT优化器
缺点 不明显 由于轻量级,部分语法不支持 Jar、依赖庞大、社区不活跃
扩展支持 自定义函数需要完成签名 自定义函数、Java函数调用 定义脚本较为复杂

Aviator虽然缺失部分语法,但考虑到实际场景中不会涉及到,其便捷的自定义函数、Lambda、以及轻量、高性能等优点,最后采用了Aviator作为重构的表达式引擎作为支撑。

四、使用表达式引擎重构

4.1 架构设计

开工前,我们对其进行一个小小的封装,作为一个基础组件方便接入以及拓展到更多的场景。

封装后提供以下特性(下文出现的规则理解为一条表达式配置):

规则配置标准化: 提供标准的规则配置存储协议接口,配置实现可以是阿波罗或者自定义后台,根据需求规模自行选择存储实现,
环境变量和规则抽象类统一封装,开发者只关心接入逻辑。

引擎扩展化: 提供快速添加自定义函数的能力,规则切面的扩展能力,开发者可以对规则进行环绕编程。

API人性化: 提供多种API支持,单个、多个规则目标匹配,自定义拦截扩展匹配。 组件需要的配置介质定义为接口,预留原生扩展能力,使用API即可完成规则的快速匹配。

4.2 配置重构

采用表达式引擎重构后由传统的KV结构转变成规则型配置,不同业务的配置按照规则维度区分,有冲突的配置采用优先级和条件叠加的方式,
配置示意:

[{"url": "红布林落地页地址",   "name": "红布林配置","ruleEl": "list.contains(labelList, 红布林标签ID)","priority": 20,"其他字段": "省略表示"  },{"url": "转转帮卖暗拍落地页地址","name": "转转帮卖暗拍配置","ruleEl": "帮卖用户ID == product.userId && 2 == product.product.searchType","priority": 20},{"url": "转转帮卖落地页地址","name": "转转帮卖配置","ruleEl": "帮卖用户ID == product.userId","priority": 20},{"url": "游戏代练陪玩落地页地址","name": "游戏代练陪玩配置","ruleEl": "代练分类ID == product.cateId || 陪玩分类ID == product.cateId","priority": 20},
]

4.3 上线小插曲

首次采用Aviator表达式引擎重构上线后,企业微信和服务管理平台第一时间收到了详情页超时的报警消息,因为超时发生在上线后,果断采取了回滚操作,恢复线上服务稳定。通过监控发现案发时刻集群Full GC严重,观察集群监控指标,类加载数量指标一直在飙升,自然联想到应该是使用本次Aviator带来的问题。 所以打算本地编写复现代码,进行复现,开启JVM类加载参数:-XX:+TraceClassLoading以便观察类加载的情况,对测试代码进行测试,随着程序的运行可以在控制台发现了大量的类加载信息打印:

[Loaded Script_1645773082560_152/982082822 from com.googlecode.aviator.Expression]
[Loaded Script_1645773082514_151/1163475645 from com.googlecode.aviator.Expression]

通过日志可以看到加载类为:com.googlecode.aviator下的Expression类,类名为:Script_当前时间戳_一个ID,那么表达式引擎执行的过程中为什么进行类加载呢,怀着疑惑的态度,阅读源代码,大致可以分析出简略执行逻辑:传入表达式 >> 是否开启缓存 >> 每次生成ASM码(否),下面分步解释

首先开始执行逻辑,准备将表达式编译

public Object execute(final String expression, final Map<String, Object> env,final boolean cached) {// 1.编译表达式Expression compiledExpression = compile(expression, cached);if (compiledExpression != null) {return compiledExpression.execute(env);} else {throw new ExpressionNotFoundException("Null compiled expression for " + expression);}}

然后根据是否启用缓存,决定实时编译还是缓存模式

public Expression compile(final String expression, final boolean cached) {if (expression == null || expression.trim().length() == 0) {throw new CompileExpressionErrorException("Blank expression");}if (cached) { // 2.缓存开启与否FutureTask<Expression> task = this.cacheExpressions.get(expression);if (task != null) {return getCompiledExpression(expression, task);}task = new FutureTask<Expression>(new Callable<Expression>() {@Overridepublic Expression call() throws Exception {return innerCompile(expression, cached);}});FutureTask<Expression> existedTask = this.cacheExpressions.putIfAbsent(expression, task);if (existedTask == null) {existedTask = task;existedTask.run();}return getCompiledExpression(expression, existedTask);} else {// 3.实时编译  return innerCompile(expression, cached);}}

最后生成字节码逻辑

public ASMCodeGenerator(final AviatorEvaluatorInstance instance,
final AviatorClassLoader classLoader, final OutputStream traceOut, final boolean trace) {this.classLoader = classLoader;this.instance = instance;this.compileEnv = new Env();this.compileEnv.setInstance(this.instance);// 上面打印的生成ASM的类名this.className = "Script_" + System.currentTimeMillis() + "_" + CLASS_COUNTER.getAndIncrement();// Auto compute framesthis.classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);visitClass();
}

到这里类一直加载的原因也找到了,Aviator默认不开启编译结果缓存,所以导致每次执行的时候都会重新编译ASM,然后加载,最后导致Full GC。开启缓存后重新上线,运行稳定。

4.4 重构前后对比

通过一段时间的需求迭代对比后,采取表达式引擎的面向配置编程的方式,从灵活度、可读性、扩展性、投入成本相对于传统的编码方式效率提升明显,效果理想。

  1. 封装后的组件大量应用到列表页,首页等场景中,动态灵活的规则配置在大促618,双11期间事半功倍。
  2. 利用表达式内置的函数和自定义函数,避免同类需求多次开发,避免重复造轮子。
  3. 常规类配置需求,利用表达式引擎基本做到需求变更无上线。

五、总结和感悟

通过对历史逻辑的分析,发掘痛点,敢于引入新技术,带来收益。联想到日常工作中也是如此,大多数时候我们总是忙于响应需求,很多时候都是按着老逻辑继续维护,只要能满足功能,哪怕麻烦点,都不会去重构它。殊不知这种习惯在慢慢侵蚀我们的心灵,吞噬我们的斗志,所以鼓励大家大胆走出那一步,相信只要你肯往前走出一步,就会带来满意的收获和成功,所以一起加油吧。


关于作者

赵天明,转转APP后端负责人。

签名:一切觉得麻烦和不爽的问题,一定是打开的方式不对。

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。

关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

表达式引擎在转转平台的实践相关推荐

  1. 规则引擎在数据治理平台的实践

    一.背景 在数据治理时,经常会遇到个性化统计分析的场景:基于数据的某些属性进行组合筛选,只有符合条件的数据才进行统计分析. 传统的实现方式是:业务人员提供筛选条件,数据开发人员在ETL任务直接开发.这 ...

  2. hutool的定时任务不支持依赖注入怎么办_分布式任务调度平台xxljob的内部原理,及在转转的落地实践...

    让世界因流转更美好 值此教师节来临之际,衷心祝愿所有的老师教师节快乐,身体健康,幸福平安,工作顺利,桃李满天下.您们辛苦了! 作者简介 · 杜云杰,架构师,转转架构部负责人,负责服务治理.MQ.云平台 ...

  3. 将军令:数据安全平台建设实践

    将军令:数据安全平台建设实践 2019年02月15日 11:16:46 美团技术团队 阅读数:42 背景 在大数据时代,数据已经成为公司的核心竞争力.此前,我们介绍了美团酒旅起源数据治理平台的建设与实 ...

  4. 京东科技埋点数据治理和平台建设实践

    本文核心内容聚焦为什么要埋点治理.埋点治理的方法论和实践.奇点一站式埋点管理平台的建设和创新功能.读者可以从全局角度深入了解埋点.埋点治理的整体思路和实践方法,落地的埋点工具和创新功能都有较高的实用参 ...

  5. 基于Apache Flink的爱奇艺实时计算平台建设实践

    导读:随着大数据的快速发展,行业大数据服务越来越重要.同时,对大数据实时计算的要求也越来越高.今天会和大家分享下爱奇艺基于Apache Flink的实时计算平台建设实践. 今天的介绍会围绕下面三点展开 ...

  6. 58同城 Elasticsearch 应用及平台建设实践

    分享嘉宾:于伯伟 58同城 高级架构师 编辑整理:陈树昌 内容来源:DataFunTalk 导读:Elasticsearch是一个分布式的搜索和分析引擎,可以用于全文检索.结构化检索和分析,并能将这三 ...

  7. 微众银行的金融级消息服务平台建设实践和思考

    来自:阿里巴巴中间件 导读: 近年来,随着微服务架构的流行,分布式消息引擎在物联网.分布式事务.实时计算和大规模缓存同步等场景中的应用日益增多.本文将分享微众银行基于RocketMQ构建消息服务平台的 ...

  8. 阿里PB级Kubernetes日志平台建设实践

    阿里PB级Kubernetes日志平台建设实践 QCon是由InfoQ主办的综合性技术盛会,每年在伦敦.北京.纽约.圣保罗.上海.旧金山召开.有幸参加这次QCon10周年大会,作为分享嘉宾在刘宇老师的 ...

  9. Flink从入门到精通100篇(二十三)-基于Apache Flink的爱奇艺实时计算平台建设实践

    前言 随着大数据的快速发展,行业大数据服务越来越重要.同时,对大数据实时计算的要求也越来越高.今天会和大家分享下爱奇艺基于Apache Flink的实时计算平台建设实践. 今天的介绍会围绕下面三点展开 ...

最新文章

  1. ​【特征工程】时序特征挖掘的奇技淫巧
  2. Linux下双网卡绑定(bonding技术)
  3. linux 命令行(给自己看的)
  4. 前端模块化开发学习之gulpbrowserify篇
  5. 新华三,定义服务器虚拟化市场新格局
  6. uvalive4840(n*n方阵的最小花费)
  7. Connection to node 0 (/192.168.204.131:9092) could not be established
  8. 算法复杂度分析(下):最好、最坏、平均、均摊等时间复杂度概述
  9. 普通树与二叉树的相互转化及哈夫曼树的了解
  10. Android 基础(十三) shape
  11. python向上取整_python向上取整-取整,向上
  12. debian系统离线安装iperf2
  13. html 自适应 音乐播放器,使用HTML5+Boostrap打造简单的音乐播放器
  14. Codeforces Round #717 (Div. 2)-A. Tit for Tat-题解
  15. 我的IT之路2011(一)
  16. 第三十三章:修改SpringBoot启动Banner
  17. swing添加按钮监听后,面板监听失效
  18. Marked.js让您的文档编辑更加轻松自如!
  19. com.lbx:xTools
  20. 骁龙660和骁龙835之间的差距到底有多大?

热门文章

  1. 什么样的人不适合当管理者?有这三种情况,你很难升上去
  2. linux 移植 内存 配置,Linux 移植篇 之 uboot的移植
  3. ccxprocess用不用自启_远程开机加远程控制,游戏玩家用这款智能插座超实用
  4. 制约中国游戏发展的十大硬伤
  5. Java中Long最大值
  6. 淘宝店铺名称怎么修改
  7. html给视频添加封面,如何给视频文件添加封面(高手来!)?
  8. android7.0清除缓存,iPhone7如何清理应用缓存 iphone7清理应用缓存教程
  9. 在uni-app中使用手机号一键登录
  10. 计算机分辨率无法调整,电脑分辨率突然变小且无法调整的解决方法