写点什么

Drools 规则引擎探究以及在 IOT 的应用

  • 2020-09-05
  • 本文字数:8799 字

    阅读完需:约 29 分钟

Drools规则引擎探究以及在IOT的应用

前言:我上家公司是做物联网的,任职在 IOT 部门,业务上针对不同类型的燃气表,水表,报警器等有不同协议,其中又包含不同厂家的表和自己公司的表。针对不同的协议,如何解析不同的协议头帧,根据头帧进行不同的复杂业务处理,后来引入了 Drools 规则引擎,通过规则的逻辑和数据的分离以及可扩展解决问题。

目前所处为淘系技术部负责天猫奢品的业务,业务背景如下:业务包含天猫奢品频道,奢品折扣频道,天猫奢品官方直营旗舰店,魅力惠旗舰店,魅力惠 APP 等。基于业务场景下会员分为店铺会员,APP 会员,天猫奢品行业会员等,而业务需要进行会员精细化的运营,通过不同的会员等级享受不同的权益,而相同的等级还需要做到根据偏好做到千人多权,如何根据复杂的业务需求变化更加精准的进行匹配,考虑 Drools 规则的逻辑数据的分离和可扩展性,接下来也会在天猫奢品的相关的会员模块中和组内成员探讨是否适合引入。下面是一些基于业务场景的总结和分享。

引入

问题引入

天猫奢品业务方为了吸引更多的新客,和提高会员的活跃度,做了一期活动,通过购买天猫奢品频道内的任意商品就赠送特殊积分,积分可以直接兑换限量的奢品商品。假如业务方给的规则如下:



主刃同学一看,这需求果然简单呀,作为团队的核心开发,这不就是小 case,5 分钟搞定,送赠品的简单代码如下:


public void exchangeGift(Integer points) {        if (points < 100) {            System.out.println("无商品");        } else if (points > 100 && points <= 200) {            System.out.println("Dior限量口红");        } else if (points > 200 && points <= 300) {            System.out.println("TF限量口红");        } else if (points > 300 && points <= 400) {            System.out.println("SK-II套装");        } else if (points > 400 && points <= 500) {            System.out.println("MCM双肩包");        } else if (points > 500) {            System.out.println("RADO雷达限量表");        }    }
复制代码


活动进行了一天,运营发现 Dior 限量版口红被兑换的很多,即将超出预期,业务方根据实际情况想改变规则,规则要求在各个层级上各加 200 分从而促进更多的消费。



主刃一看改动不大,于是五分钟后修改完代码并经测试后发布上线。活动又进行了一天,运营人员通过后台监控发现提到 200 分后,用户达成比较少,想再现有基础上再降 100 分。没办法还的改不是,主刃同学这次把配置数据放入到了阿里集团的配置中心 diamond 中,当运营改策略的,只要改一下 Diamond 的值就可以了。


核心代码编程了这样:


public void exchangeGift(Integer points) {        if (points < CommDiamondProperty.getInstance().getLevelOne()) {            System.out.println("无商品");        } else if (points > CommDiamondProperty.getInstance().getLevelOne() && points <= CommDiamondProperty.getInstance().getLevelTwo()) {            System.out.println("Dior限量口红");        } else if (points > CommDiamondProperty.getInstance().getLevelTwo() && points <= CommDiamondProperty.getInstance().getLevelThree()) {            System.out.println("TF限量口红");        } else if (points > CommDiamondProperty.getInstance().getLevelThree() && points <= CommDiamondProperty.getInstance().getLevelFour()) {            System.out.println("SK-II套装");        } else if (points > CommDiamondProperty.getInstance().getLevelFour() && points <= CommDiamondProperty.getInstance().getLevelFive()) {            System.out.println("MCM双肩包");        } else if (points > CommDiamondProperty.getInstance().getLevelFive()) {            System.out.println("RADO雷达限量表");        }    }
复制代码


新功能上线后,运营侧提出这次的分层太少了,BD 到了更多的好的赠品,需要更加精细化的运营,由以前的 5 组变成 10 组,主刃此刻的心情:kao …


那么是否有什么技术可以将活动规则和代码解耦,不管规则如何变化,执行端不用动。那就是规则引擎,那么规则引擎到底是什么东西呢?我们来看看。

规则引擎

相关介绍

目前对领域模型的常见抽象方式是面向对象思想,即简单的来说把业务逻辑抽象为对象、对象的属性和对象的方法。这样规则跟整个系统耦合,修改规则需要走全 Case 的开发测试流程发布流程,对于频繁修改的规则效率比较低,为了解决这个问题就出现了规则引擎。


今天分享的 Drools,最早由 Jboss 开发,目前由 Redhat 开源的规则引擎,选择分享 Drools 的原因是它是 Redhat 的 KIE Group 中的组件之一,可以比较方便的跟另一个组件 JBPM 工作流配合用于管理复杂的规则流;同时 Drools 的推理策略算法在经典 Rete 算法以及其它算法的基础上做了多个版本的增强,目前支持结合正向推理和反向推理优点的混合推理。


规则引擎主要完成的就是将业务规则从代码中分离出来。 在规则引擎中,利用规则语言将规则定义为 if-then 的形式,if 中定义了规则的条件,then 中定义了规则的结果。规则引擎会基于数据对这些规则进行计算,找出匹配的规则。这样,当规则需要修改时,无需进行代码级的修改,只需要修改对应的规则,可以有效减少代码的开发量和维护量。


Java 开源的规则引擎有:Drools、Easy Rules、Mandarax、IBM ILOG。使用最为广泛并且开源的是 Drools。

规则引擎优点

Drools 是用 Java 语言编写的开放源码规则引擎,使用 Rete 算法对所编写的规则求值。Drools 允许使用声明方式表达业务逻辑。可以使用非 XML 的本地语言编写规则,从而便于学习和理解。并且,还可以将 Java 代码直接嵌入到规则文件中,这令 Drools 的学习更加吸引人。


  • 声明式编程

  • 使用规则的核心优势在于可以简化对于复杂问题的逻辑表述,并对这些逻辑进行验证(规则比编码具有更好的可阅读性)。规则机制可以解决很复杂的问题,提供一个如何解决问题的说明,并说明每个决策的是如何得出的。

  • 逻辑和数据分离

  • 将业务逻辑都放在规则里的好处是业务逻辑发生变化时,可以更加方便的进行维护。尤其是这个业务逻辑是一个跨域关联多个域的逻辑时。不像原先那样将业务逻辑分散在多个对象或控制器中,业务逻辑可以被组织在一个或多个清晰定义的规则文件中。

  • 数据位于“域对象”中,业务逻辑位于“规则”中。根据项目的种类,这种分离是非常有利的。

  • 速度和可扩展性

  • 网络算法(Rete algorithm),跳跃算法(Leaps algorithm),以及有它们派生出的 Drools 的 Reteoo 算法(以及跳跃算法),提供了非常高效的方式根据业务对象的数据匹配规则。

  • Drools 的 Rete OO 算法已经是一个成熟的算法。在 Drools 的帮助下,您的应用程序变得非常可扩展。如果频繁更改请求,可以添加新规则,而无需修改现有规则。

  • 知识集中化

  • 通过使用规则,您创建一个可执行的知识库(知识库)。这是商业政策的一个真理点。理想情况下,规则是可读的,它们也可以用作文档。

规则引擎缺点

  • 复杂性提高

  • 需要学习新的规则语法

  • 引入新组件的风险

规则引擎的实现

Drools 规则引擎的结构示意图:



Rete 算法

Rete 算法最初是由卡内基梅隆大学的 Charles L.Forgy 博士在 1974 年发表的论文中所阐述的算法 , 该算法提供了专家系统的一个高效实现。自 Rete 算法提出以后 , 它就被用到一些大型的规则系统中 , 像 ILog、Jess、JBoss Rules 等都是基于 RETE 算法的规则引擎。


Rete 在拉丁语中译为”net”,即网络。Rete 匹配算法是一种进行大量模式集合和大量对象集合间比较的高效方法,通过网络筛选的方法找出所有匹配各个模式的对象和规则。


其核心思想是将分离的匹配项根据内容动态构造匹配树,以达到显著降低计算量的效果。 Rete 算法可以被分为两个部分:规则编译和规则执行。 当 Rete 算法进行事实的断言时,包含三个阶段: 匹配选择执行 ,称做 match-select-act cycle。


Reta 网络节点图如下所示:



fact: 对象之间及对象属性之间的关系,例如 Product 类及其类目下的 points、gift 属性等 Root。


Node: 根节点,Rete 网络入口。


Type Node: 对应不同的 fact 对象,也就是规则用到的 POJO,每个 fact 就是一个 TypeNode 节点 Alpha Node :对应规则里的每个条件,例如规则条件 Product(points < 100)中,points<100 就是一个 AlphaNode


Beta Node: 用于组合两个 fact 的 alpha 条件或 BetaNode 与 fact 条件的组合。


LeftInputAdapter Node: 用来对 2 个规则队形进行比较,将一个 single Object 转化为一个单对象数组(因为 BetaNode 左边入口往往是一个 list 规则队形),传播到 JoinNode 节点。


Join Node : 用于聚合 BetaNode 节点的结果。


Drools 中的 Rete 算法被称为 ReteOO,表示 Drools 为面向对象系统(Object Oriented systems)增强并优化了 Rete 算法。在 Drools 中,规则被存 放在 Production Memory(规则库)中,推理机要匹配的 facts(事实)被存在 Working Memory(工作内存)中。当时事实被插入到工作内存中后,规则引擎会把事实和规则库里的模式进行匹配,对于匹配成功的规则再由 Agenda 负责具体执行推理算法中被激发规则的结论部分,同时 Agenda 通过冲突决策策略管理这些冲突规则的执行顺序,Drools 中规则冲突决策策略有:(1) 优先级策略 (2) 复杂度优先策略 (3) 简单性优先策略 (4) 广度策略 (5) 深度策略 (6) 装载序号策略 (7) 随机策略 。

Drools 规则引擎

Drools 介绍

官网:http://www.drools.org/


官方文档:http://www.drools.org/learn/documentation.html


Drools 是一个基于 Charles Forgy’s 的 RETE 算法的,易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师人员或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。


Drools 是用 Java 语言编写的开放源码规则引擎,使用 Rete 算法对所编写的规则求值。Drools 允许使用声明方式表达业务逻辑。可以使用非 XML 的本地语言编写规则,从而便于学习和理解。并且,还可以将 Java 代码直接嵌入到规则文件中,这令 Drools 的学习更加吸引人。

Drools 相关概念

  • 事实(Fact): 对象之间及对象属性之间的关系

  • 规则(rule): 是由条件和结论构成的推理语句,一般表示为 if…Then。一个规则的 if 部分称为 LHS,then 部分称为 RHS。

  • 模式(module): 就是指 IF 语句的条件。这里 IF 条件可能是有几个更小的条件组成的大条件。模式就是指的不能在继续分割下去的最小的原子条件。


Drools 通过 事实、规则和模式相互组合来完成工作,drools 在开源规则引擎中使用率最广,但是在国内企业使用偏少,保险、支付行业使用稍多。

解决问题

Drools 有专门的规则语法 drl,就是专门描述活动的规则是如何执行的,按照主刃的需求规则如下:


rule.drl 文件内容如下:


package com.alibaba.rules
import com.alibaba.Order
rule "zero" no-loop true lock-on-active true salience 1 when $s : Order(point <= 300) then System.out.println("无赠品"); doSth($s);end
rule "giftOne" no-loop true lock-on-active true salience 1 when $s : Order(amout > 300 && amout <= 500) then System.out.println("Dior限量口红"); doSth($s);end
rule "giftTwo" no-loop true lock-on-active true salience 1 when $s : Order(amout > 500 && amout <= 700) then System.out.println("TF限量口红"); doSth($s);end
rule "giftThree" no-loop true lock-on-active true salience 1 when $s : Order(amout > 700 && amout <= 900) then System.out.println("SK-II套装"); doSth($s);end
rule "giftFour" no-loop true lock-on-active true salience 1 when $s : Order(amout > 900 && amout <= 1100) then System.out.println("MCM双肩包"); doSth($s);end
rule "giftFive" no-loop true lock-on-active true salience 1 when $s : Order(amout > 1100) then System.out.println("RADO雷达限量表"); doSth($s);end
复制代码


说明


  • package 与 Java 语言类似,drl 的头部需要有 package 和 import 的声明,package 不必和物理路径一致。

  • import 导出 java Bean 的完整路径,也可以将 Java 静态方法导入调用。

  • rule 规则名称,需要保持唯一 件,可以无限次执行。

  • no-loop 定义当前的规则是否不允许多次循环执行,默认是 false,也就是当前的规则只要满足条件,可以无限次执行。

  • lock-on-active 将 lock-on-active 属性的值设置为 true,可避免因某些 Fact 对象被修改而使已经执行过的规则再次被激活执行。

  • salience 用来设置规则执行的优先级,salience 属性的值是一个数字,数字越大执行优先级越高, 同时它的值可以是一个负数。默认情况下,规则的 salience 默认值为 0。如果不设置规则的 salience 属性,那么执行顺序是随机的。

  • when 条件语句,就是当到达什么条件的时候

  • then 根据条件的结果,来执行什么动作

  • end 规则结束


这个规则文件就是描述了,当符合什么条件的时候,应该去做什么事情,每当规则有变动的时候,我们只需要修改规则文件,然后重新加载即可生效。


这里需要有一个配置文件告诉代码规则文件 drl 在哪里,在 drools 中这个文件就是 kmodule.xml,放置到 resources/META-INF 目录下。


kmodule.xml 内容如下:


<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns="http://www.drools.org/xsd/kmodule">  <configuration>    <property key="drools.evaluator.supersetOf" value="org.mycompany.SupersetOfEvaluatorDefinition"/>  </configuration>  <kbase name="KBase1" default="true" eventProcessingMode="cloud" equalsBehavior="equality" declarativeAgenda="enabled" packages="org.domain.pkg1">    <ksession name="KSession2_1" type="stateful" default="true"/>    <ksession name="KSession2_2" type="stateless" default="false" beliefSystem="jtms"/>  </kbase>  <kbase name="KBase2" default="false" eventProcessingMode="stream" equalsBehavior="equality" declarativeAgenda="enabled" packages="org.domain.pkg2, org.domain.pkg3" includes="KBase1">    <ksession name="KSession3_1" type="stateful" default="false" clockType="realtime">      <fileLogger file="drools.log" threaded="true" interval="10"/>      <workItemHandlers>        <workItemHandler name="name" type="org.domain.WorkItemHandler"/>      </workItemHandlers>      <calendars>        <calendar name="monday" type="org.domain.Monday"/>      </calendars>      <listeners>        <ruleRuntimeEventListener type="org.domain.RuleRuntimeListener"/>        <agendaEventListener type="org.domain.FirstAgendaListener"/>        <agendaEventListener type="org.domain.SecondAgendaListener"/>        <processEventListener type="org.domain.ProcessListener"/>      </listeners>    </ksession>  </kbase></kmodule>
复制代码


说明


  • Kmodule 中可以包含一个到多个 kbase,分别对应 drl 的规则文件。

  • Kbase 需要一个唯一的 name,可以取任意字符串。

  • packages 为 drl 文件所在 resource 目录下的路径。注意区分 drl 文件中的 package 与此处的 package 不一定相同。多个包用逗号分隔。默认情况下会扫描 resources 目录下所有(包含子目录)规则文件。

  • kbase 的 default 属性,标示当前 KieBase 是不是默认的,如果是默认的则不用名称就可以查找到该 KieBase,但每个 module 最多只能有一个默认 KieBase。

  • kbase 下面可以有一个或多个 ksession,ksession 的 name 属性必须设置,且必须唯一。


rules.drl 中下方相关规则内容既可,如果活动规则动态的添加、减少也可以相应的去增加、减少规则文件既可,再也不用去动代码了。


rule "xxx"    no-loop true    lock-on-active true    salience 1    when        $s : Order(amout > yy)    then        System.out.println("xxxx");        doSth();end  
复制代码

规则中的条件操作符

Drools 提供了十二中类型比较操作符:< 、<=、>、>=、==、!=、contains、not contains、memberOf、not memberOf、matches、not matches,并且这些条件都可以组合使用。


  • 条件组合: 各种操作符可以组合使用

  • Fact 对象私有属性的操作: RHS 中对 Fact 对象 private 属性的操作必须使用 getter 和 setter 方法,而 RHS 中则必须要直接用.的方法去使用.

  • contains: 对比是否包含操作,操作的被包含目标可以是一个复杂对象也可以是一个简单的值。

  • matches: 正则表达式匹配,与 java 不同的是,不用考虑’/'的转义问题

  • memberOf: 判断某个 Fact 属性值是否在某个集合中,与 contains 不同的是他被比较的对象是一个集合,而 contains 被比较的对象是单个值或者对象。


注意:如果条件全部是【&&】可使用【,】来替代,但是两者不能混用。

规则中的结果部分

  • insert: 往当前 workingMemory 中插入一个新的 Fact 对象,会触发规则的再次执行,除非使用 no-loop 限定;

  • update: 更新

  • modify: 修改,与 update 语法不同,结果都是更新操作

  • retract: 删除

  • function: 定义一个方法,如:


function void console { System.out.println(); StringUtils.getId();// 调用外部静态方法,StringUtils 必须使用 import 导入,getId()必须是静态方法}


  • declare:可以在规则文件中定义一个 class,使用起来跟普通 java 对象相似,你可以在 RHS 部分中 new 一个并且使用 getter 和 setter 方法去操作其属性。


declare Address @author(dyzmj) // 元数据,仅用于描述信息 @createTime(2019-07-08) city : String @maxLengh(100) postno : intend


'@'是元数据定义,用于描述数据的数据~,没什么执行含义。


你可以在 RHS 部分中使用 Address address = new Address()的方法来定义一个对象。


更多语法规则可参考:Drools 规则引擎语法

Maven 中使用 Drools 的

工程结构目录

依赖文件 pom.xml

<!--规则引擎drools--><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-core</artifactId>    <version>7.0.0.Final</version></dependency><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-compiler</artifactId>    <version>7.0.0.Final</version></dependency><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-decisiontables</artifactId>    <version>7.0.0.Final</version></dependency><dependency>    <groupId>org.drools</groupId>    <artifactId>drools-templates</artifactId>    <version>7.0.0.Final</version></dependency>
<dependency> <groupId>org.kie</groupId> <artifactId>kie-api</artifactId> <version>7.0.0.Final</version></dependency>
复制代码

配置文件 kmodule.drl

package com.rulesimport model.ProtocolTypedialect "java"
rule "jk16" when $protocol : ProtocolType(data matches "^1A12.*16$" ) then $protocol.setType("xx燃气表协议"); System.out.println("触发规则1:"+$protocol.getData()); end
rule "jkstd" when $protocol : ProtocolType(data matches "18.*26",length == 10) then $protocol.setType("xx水表标准协议"); System.out.println("触发规则2:"+$protocol.getData()); end
复制代码

测试程序 Main.java

@Slf4jpublic class Main {
@Test public void test() {
KieServices ks = KieServices.Factory.get(); KieContainer kContainer = ks.getKieClasspathContainer(); KieSession kSession = kContainer.newKieSession("ksession-rules");
ProtocolType protocolType = new ProtocolType(); protocolType.setData("1A1212345616"); protocolType.setLength(12); kSession.insert(protocolType); kSession.fireAllRules(); kSession.dispose(); System.out.println(protocolType);
log.info(protocolType.getType()); }
}
复制代码

运行结果

触发规则1:1A1212345616ProtocolType(data=1A1212345616,length=12,type=xx燃气表协议)2020-08-19 18:21:35 [main] [io.example.demo.Main.test(Main.java:35)] - [INFO] xx燃气表协议
Process finished with exit code 0
复制代码

Drools 中的坑

Drools 与 SpringBoot 集成时,与热部署工具 spring-boot-devtools 存在类加载器冲突的问题,会导致所有的规则失效。在 drools 的官方网站中有人提出了这个问题,并认为是个 bug,但是 drools 的开发者认为这不是一个 bug。


地址:https://issues.jboss.org/browse/DROOLS-1540



drools 用的是 lancher classloader,而 devtools 会把我们自己编写的 model 认为是会随时更改的类,所以采用的是 Restart ClassLoader 类加载器进行加载(为了快速进行热部署),devtools 会有两个类加载器,一个 Classloader 加载那些不会改变的类(第三方 Jar 包),另一个 ClassLoader 加载会更改的类,称为 Restart ClassLoader。当采用 Launcher ClassLoader 加载的 A 类与 Restart Class Loader 的 A 类进行对比时,发现不一致,所以 drools 引擎自然无法进行识别。


所以解决办法就是将 devtools 的 maven 依赖去掉即可,或者采用 drools 官网中说明的其它方法。


本文转载自公众号淘系技术(ID:AlibabaMTT)。


原文链接


Drools规则引擎探究以及在IOT的应用


2020-09-05 10:005599

评论 1 条评论

发布
用户头像
推荐一个新兴的规则/流程引擎-ice,新的思想解决复杂规则/流程问题,以及灵活变动问题~高性能,轻量级,接入成本低~也有配套的前端配置页面
http://waitmoon.com/docs
2022-01-06 12:56
回复
没有更多了
发现更多内容

Js 异步处理演进,Callback=>Promise=>Observer

CRMEB

猫,量子力学,和手机人像摄影之变

脑极体

Apache ShardingSphere 5.0.0 内核优化及升级指南

SphereEx

数据库 开源 架构 ShardingSphere SphereEx

CSS布局(六)之居中布局

Augus

CSS 11月日更

参赛必读!! 签约计划第二季考核要求

InfoQ写作社区官方

签约计划第二季 热门活动

Python代码阅读(第64篇):角度与弧度互转

Felix

Python 编程 Code 阅读代码 Python初学者

掘金新大陆——最后一个十亿蓝海

传音开发者

献出我的膝盖!这份“基础-中级-高级”Java程序员面试集结,看完我是说直接跪了

热爱java的分享家

Java 架构 面试 程序人生 经验分享

水晶球“数据洞察”正式上线:洞悉用量趋势变化,觉察互动体验细节

声网

人工智能 水晶球 数据洞察

直播带货软件原生开发直播带货小程序平台搭建

风行无疆

淘特 Flutter 流式场景的深度优化

阿里巴巴终端技术

flutter ios android 移动应用 客户端开发

ExoPlayer播放在线TS文件无声音问题分析

Changing Lin

11月日更

百度ERNIE新突破!登顶中文医疗信息处理权威榜单CBLUE冠军

科技热闻

盲盒app源码开发盲盒小程序原生开发搭建

风行无疆

Go语言学习查缺补漏ing Day8

Regan Yue

Go 语言 11月日更

完美!华为爆出Redis宝典,原来Redis性能可压榨到极致

热爱java的分享家

Java 面试 程序人生 编程语言 经验分享

PingCode Wiki 协同编辑技术揭秘

PingCode研发中心

wiki PingCode

限量!腾讯高工用4部分讲清楚了Spring全家桶+微服务

热爱java的分享家

Java 面试 程序人生 编程语言 经验分享

智能客服"下半场":数据、技术与服务

百度大脑

人工智能

双非渣硕,在传统公司磨炼四年后成功拿到阿里offer!(附面经分享)

Geek_1df311

Java 程序员 架构 面试

IMS究竟有什么用?

鸿天hente

WorkPlus协同办公系统的优势有哪些?

WorkPlus

花了30天才肝出来,史上最全面Java设计模式总结,看完再也不会忘

Tom弹架构

Java 架构 设计模式

AI 收藏夹 Vol.002

Zilliz

工具 | pg_recovery 设计原理与源码解读

RadonDB

数据库 postgresql 源码 RadonDB

InfoQ 写作平台优质创作者签约计划第二季,我们来了!

InfoQ写作社区官方

签约计划第二季 热门活动

超强实时跟踪系统首次开源!支持跨镜头、多类别、小目标跟踪!

百度开发者中心

AI 实时跟踪

为什么Git用SHA做版本控制,而非像SVN用int数字或者是时间戳

DisonTangor

git 学习

单元测试再出发

FunTester

Java 单元测试 测试框架 spock Groovy

为什么工业巨头们偏爱自建5G私有专网!

鸿天hente

这样准备面试定能轻松斩获offer!(内附精选java面试题与答案)

Geek_1df311

Java 程序员 架构 面试

Drools规则引擎探究以及在IOT的应用_5G/IoT_刘欢(流欢)_InfoQ精选文章