KIE

版本 7.9.0

KIE 生态

图片.png

OptaPlanner 是一个本地搜索和优化的工具,独立于Drools Planner 。
UberFire 是新的workbench工程,提供类似Eclipse工作台功能。
KIE-WB 是整合了Guvnor 、drools、jbpm的uber工作台。jbpm-wb是虚的。

生命周期

  • Author 创作
    使用DRL、BPMN2、决策表、类进行知识创作
  • 构建
    将创作的知识构建为可部署的单元, JAR。
  • 测试
  • 部署
  • 使用 、管理

使用maven

使用maven构建, 遵循maven的实践规则。 KIE工程可以作为maven工程或module。 spring 中提供xml配置来代替元数据配置META-INF/kmodule.xml。

maven构建时不提供校验规则的机制, 可以通过插件实现 kie-maven-plugin 。

Kie项目具有普通maven的工程结构,唯一特点是需要包含一个kmodule.xml文件, 此文件必须放在Maven项目的resources / META-INF文件夹中,而所有其他Kie工件(如DRL或Excel文件)必须存储在resources文件夹或其下的任何其他子文件夹中。

由于工程使用默认的配置,因此最简单的kmodule.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://www.drools.org/xsd/kmodule"/>

这种情况下,kmodule将包含一个默认的KieBase 。 所有存储在resources文件夹下及其子文件夹下的规则都将被编译进去。

创建KieContainer

从类路径下读取文件创建KieContainer ;

KieServices kieServices = KieServices.Factory.get();
KieContainer kContainer = kieServices.getKieClasspathContainer();
图片.png

通过该方式,将所有的java 、kie资源都编译部署到KieContainer中,从而可以在运行时使用之。

kmodule.file

KieBase是所有应用程序知识定义的存储库。 它将包含规则,流程,函数和类型模型。 KieBase本身不包含数据;。

图片.png
图片.png

KieSession 存储和执行运行时数据。 它从KieBase创建或者当在kmodule.xml中定义时可以直接从KieContainer中创建。

如下实例, kmodule.xml中可以定义和配置多个KieBase,并且对于每个KieBase都可以创建不同的KieSession 。

<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>
      <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>

kbase的属性设置:

Attribute name Default value Admitted values Meaning
name none any 从KieContainer中获取KieBase的name
includes none 逗号分隔列表 逗号分隔的列表,kmodule中定义的其他kbase都将包含在该中
packages all any comma separated list 该packages下所有的资源文件都将包含在该kbase中
default false true, false 定义该kbase是否是默认的,若是默认的则从KieContainer中可以不传name直接创建,最多只能有一个默认kbase
equalsBehavior identity identity, equality 定义当新fact插入working memory时drools的行为。 使用identity ,则总是创建一个新的FactHandle,除非同样的对象在workingmemory中不存在。 使用equality ,则只有心插入的对象不同于(根据其提供的equal方法比较)已经存在的fact才创建。
eventProcessingMode cloud cloud, stream 当以cloud模式创建时,事件被认为是一般的facts。 当stream时允许进行时间推理
declarativeAgenda disabled disabled, enabled 是否启用Declarative Agenda

Ksession的属性:

  • name , KIESession的唯一名称标识。用于从KieContainer中获取KieSession。
  • type , stateful、stateless, 默认是stateful ;
  • default , 默认false ;当默认true时, 则允许从KieContainer中不传入name获取。
  • clockType , realtime、pseudo, 默认realtime ; 定义事件时间戳是由系统时钟还是由应用程序控制的伪时钟确定的。 该时钟对于单元测试时间规则特别有用。
  • beliefSystem , simple, jtms, defeasible ; 默认simple ; 定义KieSession使用的信任系统的类型。

如前例所示, 可以在每个KieSession上声明一个日志记录器,一个或多个WorkItemHandlers,以及3中类型的监听器:ruleRuntimeEventListener, agendaEventListener and processEventListener 。

在kmodule.xml中声明之后, 既可以使用其name从KieContainer中检索KieBase 和 KieSession。

如:

KieServices kieServices = KieServices.Factory.get();
KieContainer kContainer = kieServices.getKieClasspathContainer();

KieBase kBase1 = kContainer.getKieBase("KBase1");
KieSession kieSession1 = kContainer.newKieSession("KSession2_1");
StatelessKieSession kieSession2 = kContainer.newStatelessKieSession("KSession2_2");

需要注意的是,由于KSession2_1和KSession2_2有两种不同的类型(第一种是有状态的,而第二种是无状态的),因此必须根据声明的类型在KieContainer上调用2种不同的方法。 如果向KieContainer请求的KieSession的类型与kmodule.xml文件中声明的类型不对应,则KieContainer将抛出RuntimeException。 此外,由于KieBase和KieSession已被标记为默认值,因此可以从KieContainer获取它们而不传递任何名称。

KieContainer kContainer = ...

KieBase kBase1 = kContainer.getKieBase(); // returns KBase1
KieSession kieSession1 = kContainer.newKieSession(); // returns KSession2_1

由于Kie项目也是Maven项目,因此在pom.xml文件中声明的groupId,artifactId和version用于生成在应用程序中唯一标识此项目的ReleaseId。 这允许通过简单地将其ReleaseId传递给KieServices从项目中创建新的KieContainer。

KieServices kieServices = KieServices.Factory.get();
ReleaseId releaseId = kieServices.newReleaseId( "org.acme", "myartifact", "1.0" );
KieContainer kieContainer = kieServices.newKieContainer( releaseId );

使用maven 构建

Maven的KIE插件可确保工件资源经过验证和预编译。 要使用该插件,只需将其添加到Maven pom.xml,然后packaging使用 kjar。

  <packaging>kjar</packaging>
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.kie</groupId>
        <artifactId>kie-maven-plugin</artifactId>
        <version>7.9.0.Final</version>
        <extensions>true</extensions>
      </plugin>
    </plugins>
  </build>

该插件支持所有Drools / jBPM。 但是,如果您在Java类中使用特定的KIE注释,例如@ kie.api.Position,则需要将kie-api的编译时依赖项添加到项目中。 我们建议使用provided scope 添加KIE依赖项。 这样kjar尽可能保持轻量级,并且不依赖于任何特定的KIE版本。

在没有Maven插件的情况下构建KIE模块会将所有资源按原样复制到生成的JAR中。 当运行时加载JAR时,它将尝试构建所有资源。 如果存在编译问题,它将返回null KieContainer。 它还将编译开销推送到运行时。 通常不建议这样做,并且应始终使用Maven插件。

通过编程定义KieModule

支持通过编程的方式定义KieBase 和KieSession , 也支持通过编程将资源动态加载到项目中。 这需要使用KieFileSystem ,它是虚拟文件系统,可以通过它添加任意资源。

图片.png

可以通过KieServices获取KieFileSystem 。 kmodule.xml配置文件是必须的一步, kie提供了KieModuleModel来通过编程添加。

通过KieServices获取KieModuleModel ,配置其KieBases 和 KieSession , 转换为XML , 将XML添加到KieFileSysstem , 如下:

KieServices kieServices = KieServices.Factory.get();
KieModuleModel kieModuleModel = kieServices.newKieModuleModel();

KieBaseModel kieBaseModel1 = kieModuleModel.newKieBaseModel( "KBase1 ")
        .setDefault( true )
        .setEqualsBehavior( EqualityBehaviorOption.EQUALITY )
        .setEventProcessingMode( EventProcessingOption.STREAM );

KieSessionModel ksessionModel1 = kieBaseModel1.newKieSessionModel( "KSession1" )
        .setDefault( true )
        .setType( KieSessionModel.KieSessionType.STATEFUL )
        .setClockType( ClockTypeOption.get("realtime") );

KieFileSystem kfs = kieServices.newKieFileSystem();
kfs.writeKModuleXML(kieModuleModel.toXML());

向KieFileSystem添加其他组件:

KieFileSystem kfs = ...
kfs.write( "src/main/resources/KBase1/ruleSet1.drl", stringContainingAValidDRL )
        .write( "src/main/resources/dtable.xls",
                kieServices.getResources().newInputStreamResource( dtableFileStream ) );

上例显示可以将Kie工件添加为普通String 或 Resource 。 在后一种情况下,资源可以由KieResources工厂创建,也由KieServices提供。 KieResources提供了许多方便的工厂方法,可将InputStream,URL,File或表示文件系统路径的String转换为可由KieFileSystem管理的Resource。


图片.png

通常,可以从用于将其添加到KieFileSystem的名称的扩展名推断出资源的类型。 但是,也可以不遵循有关文件扩展名的Kie约定,并明确地将特定的ResourceType分配给资源,如下所示:

KieFileSystem kfs = ...
kfs.write( "src/main/resources/myDrl.txt",
           kieServices.getResources().newInputStreamResource( drlStream )
                      .setResourceType(ResourceType.DRL) );

向KieFileSystem中添加resource , 将KieFileSystem传给KieBuilder 来构建之。

图片.png

当KieFileSystem的内容被成功构建, 结果KieModule 被自动添加到KieRepository 。 KieRepository 是个单例,是所有KieModule的仓库。

图片.png

在此之后,可以通过KieServices使用其ReleaseId为该KieModule创建一个新的KieContainer。 但是,由于在这种情况下KieFileSystem不包含任何pom.xml文件(可以使用KieFileSystem.writePomXML方法添加一个),因此Kie无法确定KieModule的ReleaseId并为其分配默认值。 可以从KieRepository获取此默认ReleaseId,并用于标识KieRepository内部的KieModule。 以下示例显示了整个过程。

KieServices kieServices = KieServices.Factory.get();
KieFileSystem kfs = ...
kieServices.newKieBuilder( kfs ).buildAll();
KieContainer kieContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());

此时,可以从KieContainer中获取KieBases并创建新的KieSession,其方式与直接从类路径创建的KieContainer的方式完全相同。

检查编译结果是最佳做法。 KieBuilder报告了3种不同严重程度的编译结果:ERROR,WARNING和INFO。 ERROR表示项目的编译失败,没有生成KieModule,没有任何内容会添加到KieRepository。 警告和INFO结果可以忽略,但可供检查。

KieBuilder kieBuilder = kieServices.newKieBuilder( kfs ).buildAll();
assertEquals( 0, kieBuilder.getResults().getMessages( Message.Level.ERROR ).size() );

更改构建结果默认严重性

当添加一个同名的新规则时,默认会替换旧的规则,并打印出INFO结果。这在大多数时候是可以的,但有些情况不希望这么做,需要阻止规则更新并报告error。

可以通过API调用,系统属性或配置文件来完成。 从此版本开始,Drools支持规则更新和功能更新的可配置结果严重性。 要使用系统属性或配置文件对其进行配置,用户必须使用以下属性:

// sets the severity of rule updates
drools.kbuilder.severity.duplicateRule = <INFO|WARNING|ERROR>
// sets the severity of function updates
drools.kbuilder.severity.duplicateFunction = <INFO|WARNING|ERROR>

部署 Deploy

KieBase是所有知识的仓库,包括rule、process、function、type model。
KieBase本身不包含数据; KieBase可以从包括KieModule的KieContainer中获取。

图片.png

有时,如在OSGI环境, KieBase需要使用默认classloader加载不了的类型。 这种情况下,需要使用KieBaseConfiguration 来创建附加classloader 并传递KieContainer给他来创建KieBase。

KieServices kieServices = KieServices.Factory.get();
KieBaseConfiguration kbaseConf = kieServices.newKieBaseConfiguration( null, MyType.class.getClassLoader() );
KieBase kbase = kieContainer.newKieBase( kbaseConf );

KieBase创建并返回KieSession对象,它可以选择保留这些对象的引用。 当KieBase发生修改时,这些修改将应用于会话中的数据。 此引用是弱引用,它也是可选的,由布尔标志控制。

KieScanner

KieScanner允许连续监视Maven存储库,以检查是否已安装新版本的Kie项目。 在包装该项目的KieContainer中部署了一个新版本。 使用KieScanner需要kie-ci.jar在类路径上。


图片.png
KieServices kieServices = KieServices.Factory.get();
ReleaseId releaseId = kieServices.newReleaseId( "org.acme", "myartifact", "1.0-SNAPSHOT" );
KieContainer kContainer = kieServices.newKieContainer( releaseId );
KieScanner kScanner = kieServices.newKieScanner( kContainer );

// Start the KieScanner polling the Maven repository every 10 seconds
kScanner.start( 10000L );

在此示例中,KieScanner配置为以固定的时间间隔运行,但也可以通过在其上调用scanNow()方法按需运行它。如果KieScanner在Maven存储库中找到该KieContainer使用的Kie项目的更新版本,它会自动下载新版本并触发新项目的增量构建。此时,KieContainer控制下的现有KieBases和KieSessions将自动升级 - 特别是那些使用getKieBase()获得的KieBases及其相关的KieSession,以及直接使用KieContainer.newKieSession()获得的任何KieSession因此引用默认值KieBase。此外,从现在开始,从KieContainer创建的所有新KieBase和KieSession都将使用新的项目版本。请注意,在KieScanner升级之前通过newKieBase()获得的任何现有KieBase及其任何相关的KieSession都不会自动升级;这是因为通过newKieBase()获得的KieBases不受KieContainer的直接控制。

如果使用SNAPSHOT,版本范围,LATEST或RELEASE设置,KieScanner将仅对已部署的jar进行拾取更改。固定版本不会在运行时自动更新。

Maven支持许多机制来管理应用程序中的版本控制和依赖关系。 可以使用特定版本号发布模块,也可以使用SNAPSHOT后缀。 依赖关系可以指定要使用的版本范围,或者采用SNAPSHOT机制的优势。

StackOverflow为此提供了非常好的描述,如下所示。
http://stackoverflow.com/questions/30571/how-do-i-tell-maven-to-use-the-latest-version-of-a-dependency

Runing

从KieContainer 获取 KieBase

KieBase kBase = kContainer.getKieBase();

KieSession

图片.png
KieSession ksession = kbase.newKieSession();

KieRuntime

图片.png

全局Globals与fact不同,他的修改不会触发规则的重新评估。全局变量用户提供静态信息、作为RHS的服务对象,作为规则引擎的返回对象。

事件模型event model

事件提供了通知规则引擎的方法, 包括触发规则、声明对象等。 这运行将日志记录审计与应用程序分离。

KieRuntimeEventManager接口由KieRuntime实现,它提供两个接口,RuleRuntimeEventManager和ProcessEventManager。 我们这里只介绍RuleRuntimeEventManager。


图片.png

RuleRuntimeEventManager 支持监听器的添加和删除,因此working memroy 和 agenda的事件可以被监听 :


图片.png
ksession.addEventListener( new DefaultAgendaEventListener() {
    public void afterMatchFired(AfterMatchFiredEvent event) {
        super.afterMatchFired( event );
        System.out.println( event );
    }
});

Drools提供了DebugRuleRuntimeEventListener and DebugAgendaEventListener , 他们实现了打印语句, 使用如下:

ksession.addEventListener( new DebugRuleRuntimeEventListener() );

所有发出的事件都实现了KieRuntimeEvent接口,该接口可用于检索事件源自的实际KnowlegeRuntime。

图片.png

当前支持的事件由:
The events currently supported are:

MatchCreatedEvent

MatchCancelledEvent

BeforeMatchFiredEvent

AfterMatchFiredEvent

AgendaGroupPushedEvent

AgendaGroupPoppedEvent

ObjectInsertEvent

ObjectDeletedEvent

ObjectUpdatedEvent

ProcessCompletedEvent

ProcessNodeLeftEvent

ProcessNodeTriggeredEvent

ProcessStartEvent

KieRuntimeLogger

图片.png

KieRuntimeLogger使用Drools中的综合事件系统创建审计日志,该日志可用于记录应用程序的执行,以便以后使用Eclipse审计查看器等工具进行检查。

KieRuntimeLogger logger =
  KieServices.Factory.get().getLoggers().newFileLogger(ksession, "logdir/mylogfile");
...
logger.close();

Commands and commandExecutor

KIE拥有有状态或无状态会话的概念。 已经涵盖了使用标准KieRuntime的有状态会话,并且可以随着时间的推移迭代地进行。 Stateless是使用提供的数据集一次性执行KieRuntime。 它可能返回一些结果,会话在最后处理,禁止进一步的迭代交互。

上述的基础是 CommandExecutor 接口, 有状态和无状态接口都会扩展之。


图片.png
图片.png

CommandExecutor允许在这些会话上执行命令,唯一的区别是StatelessKieSession在处理会话之前在结束时执行fireAllRules()。 可以使用CommandExecutor创建命令.Javadocs使用CommandExecutor提供允许的命令的完整列表。

setGlobal和getGlobal是两个与Drools和jBPM相关的命令。

在下面设置全局调用setGlobal。 可选的boolean指示命令是否应该返回全局值作为ExecutionResults的一部分。 如果为true,则它使用与全局名称相同的名称。 如果需要替代名称,可以使用String代替布尔值。

StatelessKieSession ksession = kbase.newStatelessKieSession();
ExecutionResults bresults =
    ksession.execute( CommandFactory.newSetGlobal( "stilton", new Cheese( "stilton" ), true);
Cheese stilton = bresults.getValue( "stilton" );
StatelessKieSession ksession = kbase.newStatelessKieSession();
ExecutionResults bresults =
    ksession.execute( CommandFactory.getGlobal( "stilton" );
Cheese stilton = bresults.getValue( "stilton" );

上述例子都是使用的单条命令。 组合命令使用BatchExecution 。
组合命令是个列表, 他迭代每条命令并执行之。 这意味着可以在一个execute()中insert some objects, start a process, call fireAllRules and execute a query。

StatelessKieSession 会自动在结束时执行fireAllRules() 。

批处理中具有out标识符集的任何命令都会将其结果添加到返回的ExecutionResults实例。 让我们看一个简单的例子来看看它是如何工作的。 出于说明的目的,所呈现的示例包括来自Drools和jBPM的命令。 它们在Drool和jBPM特定部分中有更详细的介绍。

StatelessKieSession ksession = kbase.newStatelessKieSession();

List cmds = new ArrayList();
cmds.add( CommandFactory.newInsertObject( new Cheese( "stilton", 1), "stilton") );
cmds.add( CommandFactory.newStartProcess( "process cheeses" ) );
cmds.add( CommandFactory.newQuery( "cheeses" ) );
ExecutionResults bresults = ksession.execute( CommandFactory.newBatchExecution( cmds ) );
Cheese stilton = ( Cheese ) bresults.getValue( "stilton" );
QueryResults qresults = ( QueryResults ) bresults.getValue( "cheeses" );
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,717评论 6 342
  • 风,不会永远往一个方向吹。就像世界不会永远如现在呈现的这般摸样,高高低低,跌跌撞撞,保持耐心,也许下一秒,...
    菀悦阅读 732评论 1 3
  • 本周为大家介绍的是旅游意外险~阅读完本文,大约需要3分钟 什么是旅游意外险: 旅游意外险是指被保险人在保险期限内,...
    盒子小讲堂阅读 320评论 0 1