Drools 规则文件语法概述

 

概述(Overview)

以.drl为扩展名的文件,是Drools中的规则文件,规则文件的编写,遵循Drools规则语法。下面详细介绍一下Drools规则文件语法。具体参考官方文档: https://docs.jboss.org/drools/release/7.0.0.Final/drools-docs/html_single/index.html#_droolslanguagereferencechapter
DRL文件的总体结构以下:javascript

package package-name imports globals functions queries rules

对于上述元素,其顺序在drl文件中的顺序不重要,除了package-name,必须是drl文件的第一个元素。上述全部的元素都是可选的,接下来咱们将详细介绍上述元素。php

规则(Rule)组成

一个简单的规则结构以下所示:css

rule "name"    attributes    when        LHS    then        RHSend

一般,规则文件中是不须要标点符号的。即便是规则名“name”上的双引号也是可选的。attributes展现规则的执行方式,也是可选的。LHS是规则的条件部分,遵循一个固定的语法规则,在下面的内容会详细介绍。RHS一般是一个能够本地执行的代码块。html

关键字(Key Words)

从Drools 5开始,提出了“软”关键字和“硬”关键字的概念。硬关键字是保留的,不容许开发人员在编写规则文件时使用。硬关键字有以下几个:java

  • truepython

  • false程序员

  • nullexpress

软关键字是在文件内容中公认的一些字,开发人员能够在任何地方使用这些关键字,可是为了消除混淆,并不推荐开发人员在实际中使用这些关键字。软关键字有以下几个:ruby

  • lock-on-activebash

  • date-effective

  • date-expires

  • no-loop

  • auto-focus

  • activation-group

  • agenda-group

  • ruleflow-group

  • entry-point

  • duration

  • package

  • import

  • dialect

  • salience

  • enabled

  • attributes

  • rule

  • extend

  • when

  • then

  • template

  • query

  • declare

  • function

  • global

  • eval

  • not

  • in

  • or

  • and

  • exists

  • forall

  • accumulate

  • collect

  • from

  • action

  • reverse

  • result

  • end

  • over

  • init

注释(Comments)

注释是规则文件中一段会被规则引擎自动忽略的一段文本。drl文件中的注释采用类Java语法的方式,能够分为两类:单行注释和多行注释。

单行注释

单行注释能够简单的使用双斜杠"//"来标识。语法解析器会自动忽视其后的全部内容。样例以下:

rule "Testing Comments"when    // this is a single line comment    eval( true ) // this is a comment in the same line of a patternthen    // this is a comment inside a semantic code blockend

另外须要注意的是,“#”开头的单行注释在drl文件中已经被弃用。

多行注释

多行注释主要用于在代码块外对整个文件进行注释,以"/"开头和"/"结尾的之间全部的内容都会被语法解析器解释为注释。样例以下:

rule "Test Multi-line Comments"when    /* this is a multi-line comment       in the left hand side of a rule */    eval( true ) then    /* and this is a multi-line comment       in the right hand side of a rule */end

错误信息(Error Messages)

Drools 5 开始提出了标准的错误信息。标准化的目的是为了使开发者能够更准确更简单定位错误问题所在。接下来将介绍如正确理解和识别错误信息。

错误信息格式

标准的错误信息格式以下所示。

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

ErrorMessageFormat.png

 

错误信息包含如下几个部分:

  • 1st Block: 代表当前错误的错误码。

  • 2st Block: 错误可能发生的行和列。

  • 3st Block: 错误信息描述。

  • 4st Block: 指明错误发生的规则,函数,模板,查询等。

  • 5st Block: 指明错误发生于何种模式。通常不是强制性的。

错误码描述

  • 101: No viable alternative
    错误码101指明了最多见的错误,语法解析器没法找到替代方案。下面有一些常见的例子:

1: rule one 2:   when 3:     exists Foo() 4:     exits Bar()  // "exits"5:   then6: end

上述示例会产生以下错误信息:

  • [ERR 101] Line 4:4 no viable alternative at input 'exits' in rule one

上述例子中的exits != exists, 解析器找不到exits的替代方案,因而报错。下面咱们能够再看一个例子。

1: package org.drools.examples;2: rule3:   when4:     Object()5:   then6:     System.out.println("A RHS");7: end

如今,上述的代码会产生以下错误信息:

  • [ERR 101] Line 3:2 no viable alternative at input 'WHEN'

这里when是一个关键字,语法解析器在这里会遇到一个问题:rule没有文件名,而when是一个规则的条件部分。所以报错。下面还有一个相同类型错误的示例:

1: rule simple_rule2:   when3:     Student( name == "Andy ) 4:   then 5: end

这里双引号缺乏了另外一半,所以会报错。

  • 102: Mismatched input
    该错误代表,语法解析器在当前输入位置中未找到一个特定的符号。下面有一些例子:

1: rule simple_rule2:   when3:     foo3 : Bar(

上述示例会产生以下错误:

  • [ERR 102] Line 0:-1 mismatched input '<eof>' expecting ')' in rule simple_rule in pattern Bar

要解决上述问题,须要完善上述规则语句。接下来的实例会产生多个错误:

1: package org.drools.examples;2:3: rule "Avoid NPE on wrong syntax"4:   when5:     not( Cheese( ( type == "stilton", price == 10 ) || ( type == "brie", price == 15 ) ) from $cheeseList )6:   then7:     System.out.println("OK");8: end

上述代码会产生以下错误:

  • [ERR 102] Line 5:36 mismatched input ',' expecting ')' in rule "Avoid NPE on wrong syntax" in pattern Cheese

  • [ERR 101] Line 5:57 no viable alternative at input 'type' in rule "Avoid NPE on wrong syntax

  •  
 该错误信息与前一个错误相关,这里只须要将```,```用```&&```替换就行了。 * **103: Failed predicate** 出现该错误信息的缘由是一个验证的语义谓词被验证为false。一般这些语义谓词用于识别软关键字。如下是一个示例:

1: package nesting;
2: dialect "mvel"
3:
4: import org.drools.compiler.Person
5: import org.drools.compiler.Address
6:
7: fdsfdsfds
8:
9: rule "test something"
10:   when
11:     p: Person( name=="Michael" )
12:   then
13:     p.name = "other";
14:     System.out.println(p.name);
15: end

咱们能够获得以下错误信息:  * ```[ERR 103] Line 7:0 rule 'rule_key' failed predicate: {(validateIdentifierKey(DroolsSoftKeywords.RULE))}? in rule

fdsfdsfds是个无效的关键字,语法解析器没法将其识别成一个软关键字。

  • 104: Trailing semi-colon not allowed
    该错误信息与
    eval从句相关,当分号不是出如今其表达式的结尾时可能报此错误。能够看一下以下示例。

1: rule simple_rule2:   when3:     eval(abc();)4:   then5: end

因为在eval中以分号结尾,咱们能够获得以下错误信息:

  • [ERR 104] Line 3:4 trailing semi-colon not allowed in rule simple_rule

该错误很容易修复,只要移除eval中的分号便可。

  • 105: Early Exit
    drl文件中的子规则至少能被匹配选择一次,可是当子规则不能匹配任何东西的时候,就会报这个错。
    如下是一个示例:

1: template test_error2:   aa s  11;3: end

该示例会报如下错误信息:

  • [ERR 105] Line 2:2 required (…)+ loop did not match anything at input 'aa' in template test_error

  • Other Messages
    开发中可能还会遇到一些其余意想不到的问题,这些问题能够向Drools的开发团队求助。

包(Package)

package是一系列rule或其余相关构件如imports, globals的容器。这个成员之间相互关联,一个package表明了一个命名空间,其中的每一个rule的名字都是惟一的,package名字自己就是命名空间,与实际的文件和文件夹没有任何关系。常见的结构是,一个文件包含多个rule的文件就定义成一个包。如下的线路图代表了一个包中包含的全部组成元素。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

package.png


   须要注意的是,一个包必须有一个命名空间,且其声明必须遵照Java命名规范。package语句必须出如今包的首行,其余组成部分的出现顺序可有可无。其中package语句结尾的分号;是可选的。

 

import

Drools文件中的import语句功能与Java中的import语句功能相似。使用import时,必须指定对象的全称限定路径和类型名。Drools会自动导入Java相同名字的包中的全部类,也会导入java.lang.*

global

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

global.png


   global用于定义全局变量。用于使应用对象对一系列规则有效。一般,用于向规则提供全局的数据和服务,特别是一些用于规则序列的应用服务,如日志、规则序列中累加的值等。全局的变量是不会插入Woking Memory中的,另外,全局变量不要用于创建规则的条件部分,除非它是一个不会改变的常量。所有变量的改变不会通知到规则引擎,规则引擎不跟踪全局变量的变化,由于他们并无加入到Woking Memory中。全局变量使用不当,会产生不少难以想象的结果。若是多个包中同时定义了相同标识符的全局变量,那么这些全局变量必须是相同类型,并会引用一个相同的全局值。为了更好地使用全局变量,必须遵循如下规则:

 

  1. 在规则文件中定义常量,并使用这些常量。

global java.util.List myGlobalList; rule "Using a global"when    eval( true ) then    myGlobalList.add( "Hello World" ); end
  1. 在工做内存中,为全局变量设值。在从内存中获取全部的fact以前,最好将全部的全局变量设置。例如:

List list = new ArrayList(); KieSession kieSession = kiebase.newKieSession(); kieSession.setGlobal( "myGlobalList", list );

全局变量并非用来在规则间共享数据,并且最好不要用于在规则间共享数据。规则老是对工做内存的状态产生推理和反应,所以,若是想在规则之间传递数据,能够将这些数据做为facts传入工做内存。由于规则引擎并不会关心和跟踪这些全局变量的变化。

函数(Function)

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

function.png


   function提供了一种在规则源文件中插入语义代码的方式,与在普通Java类中不一样。他们须要帮助类,不然不能作任何事情。(实际上,编译器会针对这些句子自动产生帮助类。)在规则中使用函数的最主要优势就是你能够把全部逻辑放在一个地方,你能够根据须要更改这些函数的逻辑。函数一般用于在规则的then部分调用某些动做,特别是一些常常被用到的而传入参数不同的动做。
典型的
function格式以下:

 

function String hello(String name) {    return "Hello "+name+"!"; }

须要注意的是,这里咱们使用了function关键字,即便它并非Java语言的一部分。参数和返回值的定义和传入与Java语言一致。固然,参数和返回值并非必须的。另外,咱们可使用一个帮助类中的静态方法,如:Foo.hello()
  Drools支持函数的导入,咱们所要作的就是:

import function my.package.Foo.hello

不须要考虑函数的定义和导入方式,咱们能够直接在须要的地方直接使用函数名调用这个函数。例如:

rule "using a static function"when    eval( true )then    System.out.println( hello( "Bob" ) );end

类型声明(Type Declaration)

在规则引擎中,类型声明有两个目的:容许新的类型声明;容许元数据类型的声明。

  • 声明新类型:Drools工做时会将外部的普通Java对象做为事实。可是有时候使用者想要本身定义一些规则引擎能够直接使用的模型,而不用担忧用Java之类的底层语言来建立对象。另外有时候,当一个域模型已经创建,可是用户想或者须要用一些主要在推理过程当中使用的实体来完善这个模型。

  • 声明元数据:事实每每会有一些与之相关的元数据信息。元信息的样本包含的任何种类的数据都不能表明事实的属性,且在该事实类型的全部实例中都是不变的。这些元信息在规则引擎的运行和推理古城中须要被查询。

声明新类型

为了定义新类型,咱们须要使用关键字``declare,而后是一系列域,最终以end关键字结尾。一个新的fact必须有一系列域,不然规则引擎会去classpath中寻找一个存在的fact```类,若是没找到,会报错。下面给出定义新类型的几个例子。

Example . Declaring a new fact type: Address

declare Address   number : int   streetName : String   city : Stringend

上面的例子中咱们定义了一个新类型Address,这个fact有三个属性,每一个属性都具备一个Java中有效的数据类型。一样的,咱们能够再定义一个数据类型:
Example . Declaring a new fact type: Person

declare Person    name : String    dateOfBirth : java.util.Date    address : Addressend

咱们能够看一下上面的例子,dateOfBirth是Java中的java.util.Date类型,address是咱们刚才声明的类型。为了避免用写全称限定名,咱们能够先使用import来导入要使用的类型,例如:
Avoiding the need to use fully qualified class names by using import

import java.util.Date declare Person    name : String    dateOfBirth : Date    address : Address end

当咱们声明一个新的fact类型时,Drools会在编译期间生成实现自一个表示该fact类型的Java类的字节码。这个生成的Java类

Example : generated Java class for the previous Person fact typedeclaration

public class Person implements Serializable {    private String name;    private java.util.Date dateOfBirth;    private Address address;    // empty constructor    public Person() {...}    // constructor with all fields    public Person( String name, Date dateOfBirth, Address address ) {...}    // if keys are defined, constructor with keys    public Person( ...keys... ) {...}    // getters and setters    // equals/hashCode    // toString}

该类型生成的class是一个普通的Java类,能够在规则中直接使用,就向其余 fact同样。见以下例子:
Using the declared types in rules

rule "Using a declared Type"when    $p : Person( name == "Bob" ) then    // Insert Mark, who is Bob's mate.    Person mark = new Person();    mark.setName("Mark");    insert( mark ); end

声明枚举类型

DRL同时支持声明枚举类型。该类型声明须要另一种关键字enum,而后以都好分割可接收值的列表,最后以分号结束。

declare enum DaysOfWeek   SUN("Sunday"),MON("Monday"),TUE("Tuesday"),WED("Wednesday"),THU("Thursday"),FRI("Friday"),SAT("Saturday");   fullName : String end

声明完成以后,该枚举类型能够用于以后的规则中。

rule "Using a declared Enum"when   $p : Employee( dayOff == DaysOfWeek.MONDAY )then   ...end

声明元数据

在Drools中元数据会被分配给一系列不一样对象的构造:fact类型,fact属性和规则。Drools使用@符号来引出元数据,使用使用以下格式:

@metadata_key( metadata_value )

其中metadata_value是可选的。
   Drools容许声明任何任意元数据属性,可是当其余属性在运行时仅仅对查询有效时,有些属性对于规则引擎来讲具备不一样的意义。Drools容许为
fact类型和fact属性声明元数据。全部的元数据在该属性被分配到fact类型前声明,而在向一个特定属性分配值以前声明。
Example 115. Declaring metadata attributes for fact types and attributes

import java.util.Date declare Person    @author( Bob )    @dateOfCreation( 01-Feb-2009 )    name : String @key @maxLength( 30 )    dateOfBirth : Date    address : Address end

上面的例子中,声明了两个fact类型(@author@dateOfCreation)的元数据元素,另外为name属性声明了两个(@key@maxLength)元数据。其中@key没有必须值,全部括号和值均被省略了。

规则(Rule)

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

rule.png


   一个规则,指定当(when)一系列特定的条件发生时(左手边LHS),而后(then)作出一系列相应的动做(右手边RHS)。一个常见的问题就是:为何使用when而不是if,由于if一般是执行流程中特定时间点的一部分,是一个须要检查的条件。相反的,when指明的是一个不绑定于任何特定判断序列或时间点的条件判断,它在规则引擎的声明周期内的任何一个条件发生的状况下均可能触发,无论这个条件是否遇到,这些动做都会执行。
   一个规则在一个包中必须具备独一无二的名字。若是在一个DRL文件中重复定义两个相同名字的规则,在加载的时候就会报错。若是向包中添加一个名字已经存在的规则,该规则会覆盖掉以前的同名规则。若是一个规则命中存在空格符,最好使用双引号将规则名包括起来。
   规则的属性不是必须的,且属性最好写成一行。
   规则中的LHS在关键字
when的后面,一样的,RHS应该在关键字then的后面,规则最后以关键字end结尾。另外,规则不许嵌套。

 

Example . Rule Syntax Overview

rule "<name>"    <attribute>*when    <conditional element>*then    <action>*end

Example . A simple rule

rule "Approve if not rejected"  salience -100  agenda-group "approval"    when        not Rejection()        p : Policy(approved == false, policyState:status)        exists Driver(age > 25)        Process(status == policyState)    then        log("APPROVED: due to no objections.");        p.setApproved(true);end

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

rule attributes.png

规则属性

规则属性显式地声明了对规则行为的影响,有些规则属性很简单,有些规则属性是复杂的子系统的一部分,如规则流。为了从Drools中得到更多东西,咱们须要确保对每个规则属性均有正确的认识。
经常使用的规则属性有以下:

  • no-loop

    • 默认值:false

    • type: Boolean
      当规则序列更改了一个
      fact,会致使该规则会被从新触发,以致于产生一个无限循环。当设置为true时,当前规则只会被激活一次。

  • ruleflow-group

    • 默认值:N/A

    • type: String
      ruleflow是Drools的特点之一,可让你本身控制规则的命中。同一个ruleflow-group中的全部规则只有当该组激活时才能被命中。

  • lock-on-active

    • 默认值:false

    • type: Boolean
      无论什么时候
      ruleflow-groupagenda-group被激活,只要其中的全部规则将lock-on-active设置为true,那么这些规则都不会再被激活,无论一开始怎么更新,这些匹配的规则都不会被激活。这是no-loop属性的加强,由于这些变化如今不只仅是规则自身的变化。

  • salience

    • 默认值:0

    • type: Integer
      任何规则都有一个默认为0的
      salience属性,该属性能够为0,正数和负数。salience表示规则的优先级,值越大其在激活队列中的优先级越高。Drools支持使用动态的salience,可使用一个包含动态约束变量的表达式来表示。以下所示
      Dynamic Salience

rule "Fire in rank order 1,2,.."        salience( -$rank )    when        Element( $rank : rank,... )    then        ...end
  • agenda-group

    • 默认值:MAIN

    • type: String
      agenda-group容许用户将Agenda分割成多个部分以提供更多的运行控制。

  • auto-focus

    • 默认值:false

    • type: Boolean
      当一个规则被激活时
      auto-focus为true,并且该规则的agenda-group尚未focus,当该agenda-groupfocus时,容许该规则潜在命中。

  • activation-group

    • 默认值:N/A

    • type: String
      属于同一个activation-group的规则会进行惟一命中。也就是说同一个activation-group中的规则,只要有一个命中,其余的规则都会被取消激活状态,这样这些规则就不会被命中。

  • dialect

    • 默认值:as specified by the package

    • type: String
      dialect用于指明规则中使用的代码的语言种类,目前支持两种语言,"java"或"mvel"。

  • date-effective

    • 默认值:N/A

    • type: String (包含日期和时间)
      当前系统时间在date-effective以后,该规则才会被激活。

  • date-effective

    • 默认值:N/A

    • type: String (包含日期和时间)
      当前系统时间在date-effective以后,该规则不会再被激活。

  • duration

    • 默认值:无

    • type: long (包含日期和时间)
      duration用于表示一个规则在必定时间以后才会被命中,若是它仍是激活状态的话。

LHS语法

LHS是规则的条件部分的统称,由零到多条条件元素组成。若是LHS为空,默认为是条件部分一直为true。当一个新的WorkingMemory session建立的时候,会被激活和触发。
Example. Rule without a Conditional Element

rule "no CEs"when    // emptythen    ... // actions (executed once)end// The above rule is internally rewritten as:rule "eval(true)"when    eval( true ) then    ... // actions (executed once)end

LHS中的条件元素基于一个或多个模式,最经常使用的条件元素是and。固然若是LHS中有多个不互相链接的模式时,默认使用隐式的and
Implicit and

rule "2 unconnected patterns"when    Pattern1()    Pattern2() then    ... // actionsend// The above rule is internally rewritten as:rule "2 and connected patterns"when    Pattern1()    and Pattern2() then    ... // actionsend

模式
模式是最终要的条件元素,它能够隐式地匹配全部插入到WorkingMemory中的全部
fact。一个模式具备0个或多个约束条件和一个可选的模式组合。模式的结构图以下所示:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

Pattern.png


下面给出一个最简单的模式的例子

 

Person()

这里的类型为Person,该模式意味着将匹配WorkingMemory中的全部Person对象。该类型不须要是一个真实 fact对象的类。模式能够指向超类甚至是接口,这样能够匹配多个不一样类的facts。例如:

Object() // matches all objects in the working memory

模式的括号中条件定义了模式在何种条件下知足。以下所示:

Person( age == 100 )

为了引用匹配的对象,可使用一个模式绑定参数如:$p
Example . Pattern with a binding variable

rule ...when    $p : Person()then    System.out.println( "Person " + $p );end

$符号是非强制性的,只是用于在复杂的规则中方便标识,将其与变量及域区分开来。
约束

  • 什么是约束?

约束是一个返回truefalse的表达式,以下例所示:

Person( 5 < 6 )  // just an example, as constraints like this would be useless in a real pattern

约束本质上是一个与Java表达式稍微有点不一样的表达式,例如equals()等价于==。接下来咱们深刻理解一下。

  • Java Beans属性获取。

任何一个bean的属性均可以被直接使用,bean属性的获取也可使用标准的Java bean getter: getMyProperty() or isMyProperty()。例如:

//use directlyPerson( age == 50 )// this is the same as:Person( getAge() == 50 )

同时,Drools还支持嵌套的属性获取方式,如:

//use directlyPerson( address.houseNumber == 50 )// this is the same as:Person( getAddress().getHouseNumber() == 50 )

固然,约束中的条件表达式是支持Java表达式的,下面几个例子都是正确的:

Person( age == 50 ) Person( age > 100 && ( age % 10 == 0 ) ) Person( Math.round( weight / ( height * height ) ) < 25.0 )
  • 逗号分隔符 AND

逗号分隔约束,具备隐含的AND的含义。

// Person is at least 50 and weighs at least 80 kg Person( age > 50, weight > 80 ) // Person is at least 50, weighs at least 80 kg and is taller than 2 meter. Person( age > 50, weight > 80, height > 2 )

逗号运算符不能出如今复合的约束表达式中,如

// Do NOT do this: compile errorPerson( ( age > 50, weight > 80 ) || height > 2 ) // Use this insteadPerson( ( age > 50 && weight > 80 ) || height > 2 )
  • 绑定变量

属性值能够绑定到一个变量中:

// 2 persons of the same agePerson( $firstAge : age ) // bindingPerson( age == $firstAge ) // constraint expression
  • 分组访问嵌套对象属性

能够先看一个例子:

Person( name == "mark", address.city == "london", address.country == "uk" ) Person( name == "mark", address.( city == "london", country == "uk") )

也就是对嵌套对象属性的访问,能够组合在一个括号里面。

  • 内联强制类型转换

当处理嵌套对象时,每每须要将其转换成子类,能够经过使用#符号来完成。以下例所示:

Person( name == "mark", address#LongAddress.country == "uk" )

在该例子中将 Address转换成LongAddress。若是类型转换失败,该值会被认为是false。固然,类型转换也支持全称限定名称。以下所示:

Person( name == "mark", address#org.domain.LongAddress.country == "uk" )

固然,在同一个表达式中使用多级内联转换也是可行的。以下所示:

Person( name == "mark", address#LongAddress.country#DetailedCountry.population > 10000000 )

另外,Drools一样支持instanceof操做。

Person( name == "mark", address instanceof LongAddress, address.country == "uk" )
  • 特殊文字支持
    除了正常的Java文字,Drools还支持如下特殊的文字:

    • 日期文字。

查询(Query)

Domain Specific Languages


做者:圈圈_Master 

往期精彩

01 漫谈发版哪些事,好课程推荐

02 Linux的经常使用最危险的命令

03 精讲Spring&nbsp;Boot—入门+进阶+实例

04 优秀的Java程序员必须了解的GC哪些

05 互联网支付系统总体架构详解

关注我

天天进步一点点

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相关文章
相关标签/搜索