0%

Lemon

The author of Lemon and SQLite (Hipp) reports that his C programming skills were greatly enhanced by studying John Ousterhout’s original source code to Tcl.

SQLite 整体流程

在 shell.c 的 shell_exec()函数中可以看到,一般先执行 sqlite3_prepare_v2()得到 sqlite3_stmt *pSelect;然后再执行 rc = sqlite3_step(pSelect)得到结果;最后执行 sqlite3_finalize()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
sqlite3_stmt *pStmt;    /* Statement to execute. */
/* db的指针,SQL字符串,SQL长度。函数运行后得到prepared statement指针,被解析的字符串的末尾
** 详情参考下方1.部分
*/
rc = sqlite3_prepare_v2(db, zSql, -1, &pStmt, &zLeftover);
/*
* 执行pStmt的内容,pStmt被转换为Vdbe。Vdbe中主要存储
* sqlite3 *db;// The database connection that owns this statement
* Op *aOp;// Space to hold the virtual machine's program,vdbe读取这里存储的字节码并执行
* Mem *aColName; /* Column names to return */
* Mem *pResultSet; /* Pointer to an array of results */
* 执行pStmt的字节码,并且将结果存储到pStmt->pResultSet中
*/
rc = sqlite3_step(pStmt);
/*
** 通过回调函数,来将结果返回
** shell.c中这个static int shell_callback(void *pArg, int nArg, char **azArg, char **azCol, int *aiType)xCallback的实现类
** pArg存储的是callback_data结构体指针,里面存储了pStmt。pArg->mode决定了显示方式,结果通过fprintf()输出到控制台。
** switch( p->mode ){
** case MODE_List:
** fprintf(p->out, "%s", z);//打印值,z为值,p->out指向输出的文件(Linux包括输出设备也是文件)
** fprintf(p->out, "%s", p->separator);//打印分隔符,p->separator为"|",p->out指向输出的文件(Linux包括输出设备也是文件)
** }
** 从pStmt->pResultSet中提取列数量,列内容,列名字,列类型等信息。
** nArg存储的是列的数量
** azArg存储的是列的内容
** azCol存储的是列的名字
** aiType存储的是列的类型,比如整数,字符类型等。使用宏定义了类型
** SQLITE_INTEGER 1
** SQLITE_FLOAT 2
** SQLITE_TEXT 3
** SQLITE_BLOB 4
** SQLITE_NULL 5
*/
xCallback(pArg, nCol, azVals, azCols, aiTypes)
/*
** 销毁pStmt指向的虚拟机vdbe。返回一个success/failure 整数码。
** 如果成功则将zSql指向下一个要执行的SQL语句。最后将callback_data结构体指针pArg,成员pStmt设置为null
*/
rc = sqlite3_finalize(pStmt);
if( rc==SQLITE_OK ){
zSql = zLeftover;
while( isspace(zSql[0]) ) zSql++;
}else if( pzErrMsg ){
*pzErrMsg = save_err_msg(db);
}

/* clear saved stmt handle */
if( pArg ){
pArg->pStmt = NULL;
}
    1. sqlite3_prepare_v2
    • 1.1 prepare.c->sqlite3_prepare_v2->sqlite3LockAndPrepare->sqlite3Prepare(const char *zSql,sqlite3_stmt **ppStmt,...)输入的 UTF-8 encoded SQL zSql 被编译为一个 prepared statement 指针 ppStmt 并返回。同时内部初始化 Parse。

      • 1.1.1 Parse *pParse = sqlite3StackAllocZero(db, sizeof(\*pParse));Parse 是一个 SQL parser 上下文,它从 parser 传递到 sql 解析相关内容到所有 parser action routine。相当于是解析过程中的一个全局变量,但是并未声明为全局变量,而是以入参方式进行传递。
        Parse 的主要成员包括
        1
        2
        3
        4
        5
        sqlite3 *db;         /* The main database structure */
        Vdbe *pVdbe; /* An engine for executing database bytecode */
        Token sLastToken; /* The last token parsed */
        Table *pNewTable; /* A table being constructed by CREATE TABLE */
        table,column,lock等相关信息
      • 1.1.2 sqlite3RunParser(pParse, zSqlCopy, &zErrMsg);调用 sqlite3RunParser()对 zSqlCopy 进行解析。内部完成对 pParse->pVdbe 初始化赋值
      • 1.1.3 *ppStmt = (sqlite3_stmt*)pParse->pVdbe;
    • 1.2 tokenize.c->sqlite3RunParser(Parse *pParse, const char *zSql, char **pzErrMsg)对 zSql 指向 SQL 字符串,对其进行解析,pParse 作为解析上下文被传入,函数将返回 SQLITE_的 status code。

      • 1.2.1 pEngine = sqlite3ParserAlloc((void*(*)(size_t))sqlite3Malloc)创建解析器 pEngine, LEMON-generated LALR(1) parser。

      • 1.2.2

        1
        2
        3
        4
        5
        6
        7
        8
        9
        while( !db->mallocFailed && zSql[i]!=0 ){
        pParse->sLastToken.z = &zSql[i];
        pParse->sLastToken.n = sqlite3GetToken((unsigned char\*)&zSql[i],&tokenType);
        ....
        switch( tokenType ){
        case TK_SPACE: break;
        case TK_SEMI: break;
        default: sqlite3Parser(pEngine, tokenType, pParse->sLastToken, pParse);
        }

        循环处理 SQL,sqlite3GetToken()将从指定数组位置开始解析,token 类型解析后存入 tokenType,token 长度最为函数返回值。
        然后,switch 语句根据 token 类型进行处理,一般都会进入 default 中。里面调用 sqlite3Parser(),将待解析的 token,pParse 解析上下文传递给解析器 pEngine。经过若干次循环,以及解析递归,在 sqlite3Parser()里完成对 pParse->pVdbe 初始化赋值。

    • 1.3 parse.c->sqlite3Parser()
      在 sqlite3Parser()里会读入 token,并对 token 组成的字符串进行分析匹配到合适的语法规则,然后调用相应的动作。语法规则和动作定义在 parse.y 中,动作中一般会调用 build.c,select.c 中定义好的动作函数,来进行 vdbe 初始化和字节码生成等操作。

      1
      2
      3
      4
      5
      6
      void sqlite3Parser(
      void *yyp, /* The parser */
      int yymajor, /* The major token code number */
      sqlite3ParserTOKENTYPE yyminor /* The value for the token */
      sqlite3ParserARG_PDECL /* Optional %extra_argument parameter */
      )

      关键逻辑,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      do{
      yyact = yy_find_shift_action(yypParser,(YYCODETYPE)yymajor);
      if( yyact<YYNSTATE ){
      assert( !yyendofinput ); /* Impossible to shift the $ token */
      yy_shift(yypParser,yyact,yymajor,&yyminorunion);
      yypParser->yyerrcnt--;
      yymajor = YYNOCODE;
      }else if( yyact < YYNSTATE + YYNRULE ){
      yy_reduce(yypParser,yyact-YYNSTATE);
      }else{
      assert( yyact == YY_ERROR_ACTION );
      yy_syntax_error(yypParser,yymajor,yyminorunion);
      yy_destructor(yypParser,(YYCODETYPE)yymajor,&yyminorunion);
      yymajor = YYNOCODE;
      }
      } while(yymajor!=YYNOCODE && yypParser->yyidx>=0)
      • 1.3.1 parse.y
        中定义的 DROP TABLE 和 SELECT 规则如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      ////////////////////////// The DROP TABLE /////////////////////////////////////
      //
      cmd ::= DROP TABLE ifexists(E) fullname(X). {
      sqlite3DropTable(pParse, X, 0, E);
      }
      %type ifexists {int}
      ifexists(A) ::= IF EXISTS. {A = 1;}
      ifexists(A) ::= . {A = 0;}
      //////////////////////// The SELECT statement /////////////////////////////////
      //
      cmd ::= select(X). {
      SelectDest dest = {SRT_Output, 0, 0, 0, 0};
      sqlite3Select(pParse, X, &dest);
      sqlite3SelectDelete(pParse->db, X);
      }

      • 1.3.2 parse.c->yy_reduce(yyParser *yypParser, int yyruleno)完成一个 reduce 动作和相关 shift 动作。
        yypParser 是解析器,yyruleno 是规则号。这里关键逻辑是,根据规则号调用相应的处理逻辑,如下是 DROP TABLE 的处理逻辑。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      switch( yyruleno ){
      ...
      case 107: /* cmd ::= DROP TABLE ifexists fullname */
      {
      sqlite3DropTable(pParse, yymsp[0].minor.yy259, 0, yymsp[-1].minor.yy4);
      }
      break;
      case 112: /* cmd ::= select */
      {
      SelectDest dest = {SRT_Output, 0, 0, 0, 0};
      sqlite3Select(pParse, yymsp[0].minor.yy387, &dest);
      sqlite3SelectDelete(pParse->db, yymsp[0].minor.yy387);
      }
      break;
      ...
      }
    • 1.4 代码生成器

      - 1.4.1 `build.c->sqlite3DropTable(Parse *pParse, SrcList *pName, int isView, int noErr)`
        这里是根据 pName 中的表名,来将表从系统中删除。关键逻辑
        
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      /* Generate code to remove the table from the master table
      ** on disk.
      */
      v = sqlite3GetVdbe(pParse);
      if( v ){
      Trigger *pTrigger;
      Db *pDb = &db->aDb[iDb];
      sqlite3BeginWriteOperation(pParse, 1, iDb);
      sqlite3FkDropTable(pParse, pName, pTab);
      /* Drop all SQLITE_MASTER table and index entries that refer to the
      ** table. The program name loops through the master table and deletes
      ** every row that refers to a table of the same name as the one being
      ** dropped. Triggers are handled seperately because a trigger can be
      ** created in the temp database that refers to a table in another
      ** database.
      */
      sqlite3NestedParse(pParse,
      "DELETE FROM %Q.%s WHERE tbl_name=%Q and type!='trigger'",
      pDb->zName, SCHEMA_TABLE(iDb), pTab->zName);
      sqlite3ClearStatTables(pParse, iDb, "tbl", pTab->zName);
      if( !isView && !IsVirtual(pTab) ){
      destroyTable(pParse, pTab);
      }

      /* Remove the table entry from SQLite's internal schema and modify
      ** the schema cookie.
      */
      if( IsVirtual(pTab) ){
      sqlite3VdbeAddOp4(v, OP_VDestroy, iDb, 0, 0, pTab->zName, 0);
      }
      sqlite3VdbeAddOp4(v, OP_DropTable, iDb, 0, 0, pTab->zName, 0);
      sqlite3ChangeCookie(pParse, iDb);
      }
      这里根据 pParse 解析上下文来获得一个 vdbe,然后开始生成字节码通过 sqlite3VdbeAddOp4()函数。将操作指令载入 v 指向的 vdbe 中。最终在 在 sqlite3_prepare_v2 产生的结果 sqlite3_stmt \*\*ppStmt 中返回。 - 1.4.2 `select.c->sqlite3Select()` 这里产生 select 的字节码,包括各种 from,group by,limit 等。当处理 where 语句是调用 sqlite3WhereBegin(),这里做查询优化,sqlite3WhereEnd()负责优化后的清理工作。 - 1.4.2.1 ‘where.c->sqlite3WhereBegin()’ sqlite3WhereBegin 函数中调用 exprAnalyzeAll 分析 Where 语法树,嵌套循环调用 bestBtreeIndex 函数计算每种查询策略代价,选取最优代价的策略生成 VDBE OPCode。 - `初始化` 为返回值 WhereInfo 初始化及分配内存空间,一次分配 WhereInfo 中的 a[]、WhereClause 及 WhereMaskSet。
      1
      2
      3
      4
      5
      6
      7
      db = pParse->db;
      nByteWInfo = ROUND8(sizeof(WhereInfo)+(nTabList-1)*sizeof(WhereLevel));
      pWInfo = sqlite3DbMallocZero(db,
      nByteWInfo +
      sizeof(WhereClause) +
      sizeof(WhereMaskSet)
      );
      - `whereSplit(pWC, pWhere, TK_AND)` 初始化 WhereClause pWC,调用 whereSplit 函数进行分解 Where 语句中以 AND 操作符分割的子语句。
      1
      2
      3
      4
      initMaskSet(pMaskSet);
      whereClauseInit(pWC, pParse, pMaskSet, wctrlFlags);
      sqlite3ExprCodeConstants(pParse, pWhere);
      whereSplit(pWC, pWhere, TK_AND); /* IMP: R-15842-53296 */
      - `exprAnalyzeAll()` 调用 exprAnalyzeAll 函数分析所有的子表达式,处理前面查询语句的几种查询优化处理方法,例如 BETWEEN、同属性多个 OR 连接、LIKE 语句等的处理。 - `bestBtreeIndex()`?? `bestBtreeIndex(pParse, pWC, pTabItem, mask, notReady, pOrderBy,pDist, &sCost);`将计算的代价结果存入 sCost 中。计算对当前循环的表查询策略的代价,通过分析能否使用索引、记录数量、排序等因素估算查询开销代价。 为某个特定的表寻找最佳查询计划。确定最好的查询计划和成本, 写入最后一个传入的 WhereCost 对象参数。 分析影响查询策略代价的因数计算查询开销,因素主要包括: - 能否使用索引; - 记录数量; - 是否排序; - 查询条件的类型; - 如果有 INDEX BY 子句 (pSrc->pIndex) 附加到 SQL 语句中, 此函数只考虑使用指定的索引。如果没有这样的计划找到,那么返回的成本就是 SQLITE_BIG_DBL。 - 如果没有索引子句 (pSrc->notIndexed!=0) 附加到的表中的 SELECT 语句,认为没有索引。然而,所选的计划仍然可以利用内置的 rowid 主键索引。 - `codeOneLoopStart()`?? 调用 codeOneLoopStart 函数,生成查询处理 OPCode,针对 for 循环中的每次迭代生成单个虚拟机程序的嵌套循环,并且根据优化分析得到的结果针对 5 种查询策略生成每层循环不同的 OPCode。 根据五种查询策略生成OPCode,包括: RowId等值查询; RowId范围查询; 使用索引等值或范围查询; OR查询; 全表扫描; 基于虚表(Virtual Table)的扫描。 - 虚表等值处理
      1
      2
      3
      4
      5
      6
      7
      8
      if(  (pLevel->plan.wsFlags & WHERE_VIRTUALTABLE)!=0 ){
      /* Case 0: The table is a virtual-table. Use the VFilter and VNext to access the data.
      表是一个虚拟表。使用VFilter和VNext访问数据
      */
      ......
      sqlite3VdbeAddOp2(v, OP_Integer, pVtabIdx->idxNum, iReg);
      sqlite3VdbeAddOp2(v, OP_Integer, j-1, iReg+1);
      sqlite3VdbeAddOp4(v, OP_VFilter, iCur, addrBrk, iReg, pVtabIdx->idxStr, pVtabIdx->needToFreeIdxStr ? P4_MPRINTF : P4_STATIC);
      - RowId等值查询
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      if( pLevel->plan.wsFlags & WHERE_ROWID_EQ ){
      /* Case 1: We can directly reference a single row using an
      ** equality comparison against the ROWID field. Or
      ** we reference multiple rows using a "rowid IN (...)" construct.
      ** 可以直接引用一个单行对ROWID字段使用相等的比较。
      ** 或者使用引用多个行“rowid(…)IN”结构。
      */
      ......
      sqlite3VdbeAddOp2(v, OP_MustBeInt, iRowidReg, addrNxt);
      sqlite3VdbeAddOp3(v, OP_NotExists, iCur, addrNxt, iRowidReg);
      sqlite3ExprCacheStore(pParse, iCur, -1, iRowidReg);
      - RowId非等值扫描:
      1
      2
      3
      4
      5
      6
      else if( pLevel->plan.wsFlags & WHERE_ROWID_RANGE ){
      /* Case 2: We have an inequality comparison against the ROWID field.
      对ROWID字段非等值的比较
      */
      ......
      sqlite3VdbeAddOp3(v, aMoveOp[pX->op-TK_GT], iCur, addrBrk, r1);
      - 使用索引的范围或等值查询
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
         else if( pLevel->plan.wsFlags & (WHERE_COLUMN_RANGE|WHERE_COLUMN_EQ) ){
      /* Case 3: A scan using an index.
      where子句可能有0个或者多个相等关系("==" or "IN" )涉及到N个最左连接索引它可能也有不等关系(>, <, >= or <=)在索引集中紧跟着N相等关系。只有最右连接集才可以成为相等,剩余的必须用于 "==" and "IN" 。
      例如以(x,y,z)建立索引。下面所有的子句都是最优的。
              x=5
      ** x=5 AND y=10
      ** x=5 AND y<10
      ** x=5 AND y>5 AND y<10
      ** x=5 AND y=5 AND z<=10**

      - OR查询
      1
      2
      3
      if( pLevel->plan.wsFlags & WHERE_MULTI_OR ){
      /* Case 4: Two or more separately indexed terms connected by OR
      **两个或两个以上的独立索引术语或连接
      - 全表查询
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      else{
      /* Case 5: There is no usable index. We must do a complete scan of the entire table.
      没有可用的索引。我们必须完整的扫描整个表。
      */
      static const u8 aStep[] = { OP_Next, OP_Prev };
      static const u8 aStart[] = { OP_Rewind, OP_Last };
      assert( bRev==0 || bRev==1 );
      assert( omitTable==0 );
      pLevel->op = aStep[bRev];
      pLevel->p1 = iCur;
      pLevel->p2 = 1 + sqlite3VdbeAddOp2(v, aStart[bRev], iCur, addrBrk);
      pLevel->p5 = SQLITE_STMTSTATUS_FULLSCAN_STEP;
      }
  • 2.vdbeapi.c->sqlite3_step()

    • 2.1 vdbeapi.c->sqlite3_step()->sqlite3Step()->sqlite3VdbeExec(Vdbe *p)
      sqlite3_step 将 sqlite3_prepare_v2 产生的结果 sqlite3_stmt **ppStmt,转换为 Vdbe *p。然后继续出。
      主要逻辑如下先执行初始化,然后开始循环处理字节码并在 switch 中进行对应类型字节码的处理。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    SQLITE_PRIVATE int sqlite3VdbeExec(Vdbe *p){
    int pc=0; /* The program counter */
    Op *aOp = p->aOp; /* Copy of p->aOp */
    Op *pOp; /* Current operation */
    int rc = SQLITE_OK; /* Value to return */
    sqlite3 *db = p->db; /* The database */
    u8 resetSchemaOnFault = 0; /* Reset schema after an error if positive */
    u8 encoding = ENC(db); /* The database encoding */
    Mem *aMem = p->aMem; /* Copy of p->aMem */
    Mem *pIn1 = 0; /* 1st input operand */
    Mem *pIn2 = 0; /* 2nd input operand */
    Mem *pIn3 = 0; /* 3rd input operand */
    Mem *pOut = 0; /* Output operand */
    int iCompare = 0; /* Result of last OP_Compare operation */
    int *aPermute = 0; /* Permutation of columns for OP_Compare */
    i64 lastRowid = db->lastRowid; /* Saved value of the last insert ROWID */
    for(pc=p->pc; rc==SQLITE_OK; pc++){ //for循环中逐条执行指令,pc是指令计数器
    assert( pc>=0 && pc<p->nOp );
    if( db->mallocFailed ) goto no_mem;
    switch( pOp->opcode ){
    //switch语句,每一个case都是在VDBE里执行一个单独的指令,例如下面
    /* Opcode: Not P1 P2 * * *
    **
    ** Interpret the value in register P1 as a boolean value. Store the
    ** boolean complement in register P2. If the value in register P1 is
    ** NULL, then a NULL is stored in P2.
    */
    case OP_Not: { /* same as TK_NOT, in1, out2 */
    pIn1 = &aMem[pOp->p1];
    pOut = &aMem[pOp->p2];
    if( pIn1->flags & MEM_Null ){
    sqlite3VdbeMemSetNull(pOut);
    }else{
    sqlite3VdbeMemSetInt64(pOut, !sqlite3VdbeIntValue(pIn1));
    }
    break;
    }
    }
    }
    }

  • 3.vdbeapi.c->sqlite3_finalize(sqlite3_stmt *pStmt)
    这里将销毁由 sqlite3_compile()创建的虚拟机,pStmt->pVdbe 指向了这个虚拟机。sqlite3VdbeFinalize(v)具体执行了销毁操作。释放掉 pVdbe 指向的内存。

后端

源代码编译和调试

通过修改 3.7 版本的 makefile 中sqlite3$(TEXE) 可以使用 gdb 断点调试在 sqlite3.c 中

1
2
3
4
sqlite3$(TEXE):	$(TOP)/src/shell.c sqlite3.c
$(LTLINK) $(READLINE_FLAGS) $(SHELL_OPT) -o $@ \
$(TOP)/src/shell.c sqlite3.c \
$(LIBREADLINE) $(TLIBS) -rpath "$(libdir)"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
CREATE INDEX index_dept ON DEPARTMENT (DEPT);
CREATE INDEX index_emp ON DEPARTMENT (EMP_ID);
CREATE INDEX index_dept2 ON DEPARTMENT2 (DEPT);
CREATE INDEX index_emp2 ON DEPARTMENT2 (EMP_ID);


DROP TABLE DEPARTMENT;
DROP TABLE DEPARTMENT2;
CREATE TABLE DEPARTMENT(ID INT PRIMARY KEY NOT NULL,DEPT CHAR(50) NOT NULL,EMP_ID INT NOT NULL);
CREATE TABLE DEPARTMENT2(ID INT PRIMARY KEY NOT NULL,DEPT CHAR(50) NOT NULL,EMP_ID INT NOT NULL);
CREATE INDEX index_dept_emp ON DEPARTMENT (ID,DEPT,EMP_ID);
INSERT INTO DEPARTMENT VALUES (1,'asd',1);
INSERT INTO DEPARTMENT VALUES (2,'asd2',2);
INSERT INTO DEPARTMENT VALUES (3,'asd3',3);
INSERT INTO DEPARTMENT VALUES (4,'asd4',4);
INSERT INTO DEPARTMENT VALUES (5,'asd5',5);
INSERT INTO DEPARTMENT VALUES (6,'asd6',6);
INSERT INTO DEPARTMENT VALUES (7,'asd7',7);
INSERT INTO DEPARTMENT VALUES (8,'asd8',8);
INSERT INTO DEPARTMENT VALUES (9,'asd9',9);
INSERT INTO DEPARTMENT VALUES (10,'asd10',10);
INSERT INTO DEPARTMENT VALUES (11,'asd11',11);
INSERT INTO DEPARTMENT VALUES (12,'asd12',12);
INSERT INTO DEPARTMENT VALUES (13,'asd13',13);
INSERT INTO DEPARTMENT VALUES (14,'asd14',14);
INSERT INTO DEPARTMENT VALUES (15,'asd15',15);
INSERT INTO DEPARTMENT VALUES (16,'asd16',16);
INSERT INTO DEPARTMENT VALUES (17,'asd17',17);
INSERT INTO DEPARTMENT VALUES (18,'asd18',18);
INSERT INTO DEPARTMENT VALUES (19,'asd19',19);
INSERT INTO DEPARTMENT VALUES (20,'asd20',20);
INSERT INTO DEPARTMENT VALUES (21,'asd21',21);
INSERT INTO DEPARTMENT2 VALUES (1,'asd1',1);
INSERT INTO DEPARTMENT2 VALUES (2,'asd2',2);
SELECT DEPARTMENT.ID, DEPARTMENT2.ID, DEPARTMENT.DEPT, DEPARTMENT.EMP_ID FROM DEPARTMENT,DEPARTMENT2 WHERE DEPARTMENT.ID = DEPARTMENT2.ID AND DEPARTMENT.DEPT = 'asd' AND DEPARTMENT.EMP_ID IN (1,2,3);
SELECT DEPARTMENT.ID, DEPARTMENT2.ID, DEPARTMENT.DEPT, DEPARTMENT.EMP_ID FROM DEPARTMENT,DEPARTMENT2 WHERE DEPARTMENT.ID = DEPARTMENT2.ID AND DEPARTMENT.EMP_ID>3 AND DEPARTMENT.EMP_ID<15;
SELECT DEPARTMENT.ID, DEPARTMENT.DEPT, DEPARTMENT.EMP_ID FROM DEPARTMENT WHERE DEPARTMENT.DEPT>'asd8' AND DEPARTMENT.EMP_ID<15;
SELECT ID, DEPT, EMP_ID FROM DEPARTMENT WHERE ID IN(2,13,14,15,16) AND DEPT>'asd15' AND EMP_ID>15;

OS 层

sqlite3.h定义了主要的数据结构
sqlite3_vfs: 定义了 vps 的名字,核心方法:比如创建文件,删除文件等。
sqlite3_io_methods: 定义了操作文件的方法,比如读文件,写文件等。
sqlite3_file:代表一个打开了的文件,由 sqlite3_vfs 中的 XOpen 方法返回。sqlite3_file 内部存储一个指向 sqlite3_io_methods 的指针。
test_demovfs.c是一个 sqlite3_vfs 实现类的样例。C 语言中通过给结构体的函数指针赋值,来完成接口和实现类的连接。

1
2
3
static int demoDelete(sqlite3_vfs *pVfs, const char *zPath, int dirSync){
//这方法在删除的时候,使用unlink删除文件后。还从zPath中提取目录,然后将删除的工作同步更改到目录中。也就是需要在文件系统中删除对应文件的元数据。这里值得研究!!!linux如何彻底删除一个文件。参考unix高级环境编程。
}

Pager 层

Storage

Database File Structure

  1. SQLite 将每个 database file 分成了若干个固定大小的区域叫 page。这样 database file 就变成了数组,如下:
  2. page size 默认为 1024 bytes。这个值可以在编译时指定或者在创建第一个 table 之前使用命令page_size pragma
  3. page type.包含四中类型:free,tree,pointer-map,lock-byte.
  4. 数据库的元数据存储在第一个 page,其他 page 可以存储任意 page type 的数据。第一个 page 的结构如下

    file header 中 1 前 100 字节存储 database file 结构信息,中间部分存储(master catalog table) B+树用于跟踪文件中的其他 page。header 部分的内容如下:
  5. freelist 数据结构。它按照如下结构组织数据。trunk pointer 用于指向下一个节点,number of leaves 用于存储叶子指针的数量,leaf page numbers 用于存储 leaf pages 的数量。当 page 不被使用时,SQLite 将它存储进 Freelist 并不换给文件系统。后面有信息 page 需求时,先从 freelist 中找到可用 page。如果想把 page 归还,可以使用 vacuum 命令(手动或自动)清空 page。

Journal File Structure

SQLite 使用三种日志文件:rollback journal, statement journal, master journal.

  • Rollback journal.这个文件是临时文件,与 database file 位于同目录下。每次写事务时创建日志,每次结束时删除日志。
    • segment header structure.
      • 存储记录数,数据库初始 page 数量,sector size, page size,魔数(确认文件类型)等
      • 日志文件可以被保留,日志文件需要包含有效的 segment header。
      • SQLite 支持异步事务,也就是不强制刷新日志和 database file。这样性能好,但是不具备失败恢复性。
    • log record structure
      • 数据结构如下:checksum 十分重要,因为在新建的文件中,log record 中的数据有可能是早先被删除的文件的内容,如果没有 checksum 做检查,那么脏数据就会被读取。这种将 page number 放在头部,checksum 放在尾部的方式,可以很好地在宕机后发现有问题的数据区域。因为一个区域的写往往是顺序的,如果头部和尾部是正确的,那么中间应该也是正确的。
  • Statement journal
    这个 Statement journal 被用来从 statement 失败中恢复数据库。当 Statement 执行成功后,这个文件被删除。
    通过设置项,可以保留 Statement Journal。
  • 多数据库事务日志和主日志
    通常一个数据库只有一个 database file,一个 database file 对应一个事务。但是通过 attach 命令,可以将多个 database file 绑定在一个 sqlite 中。这样在一次事务执行过程中,可以操作多个 database file,每个 database file 都有自己单独的 rollback journal file。如果让多个子 journal file 保持全局原子性,需要使用 master journal。它位于 database file 同级目录,以-mj 结尾。
    每个子 journal file,拥有 master journal 的名字。如下是子 journal file 的结构

日志管理

Journal 用于恢复,当事务失败或系统宕机时。每个数据库一个 Journal 文件,与数据库文件位于同目录,‘-journal’结尾命名。每当写事务开启时创建 journal,完成后关闭 journal。
SQLite 的使用的日志方式是最简单的且不是很高效。它在 page 级别粒度上记录旧值,使用 undo 方式恢复。SQLite 把将要被改变的数据所在的页的完整内容 copy 进 journal 中。journal 记录 database file 的尺寸在 journal segment header 中,当 journal 文件被创建是。如果 database file 被事务扩大了,那么 journal file 可以让 database file 回滚到原来大小。

  • Journaled Page 跟踪:SQLite 使用内存 bit map 数据结构来跟踪哪个 page 被当前事务记录了日志。
  • 不要给 database file 和 journal file 命名别名。
  • WAL。在修改 database file 在 disk 生效之前,一定保证先把日志刷盘,以保证可以在宕机后修复。
  • 异步事务模式。也就是在提交时不将 journal 文件刷盘,这样速度很快,但是没有恢复能力。临时数据库默认是异步事务,因为宕机时我们不需要恢复临时数据库。
  • 子事务管理。每个 Statement 子事务通过用户事务获取锁,它并不 刷盘,因为它不承担恢复的责任。一个 log record 写入 Statement journal 仅当在 Statement 执行前相应 page 已经写在了用户事务中。

事务管理

事务类型

几乎所有数据库系统都是用锁来控制并发,使用日志来恢复。一开始 DBMS 首先将修改写入日志的磁盘中,然后再修改数据。当宕机发生时,撤销未完成的事务,重做已完成的事务。

  • 系统事务
    SQLite 中读写数据都必须在事务中进行,但是不需要显示指明事务类型,SQLite 根据操作自行分辨。对于 SELECT 语句 SQLite 开始读事务,对于非 SELECT 语句 SQLite 开始读事务,再将读事务升级为写事务。一个 connection 可以同时执行多个 SELECT 语句,但是非 SELECT 在 connection 只能单个执行。也就是多个读事务+单个写事务可以并发执行在一个 connection 上,但多个写事务不行。
    非 SELECT 语句被原子地执行,执行前 SQLite 获取锁,执行完成释放锁。
  • 用户事务
    默认的系统事务,在执行多个写操作时效率很低,因为每执行一个写操作都要操作日志和锁。这时可以使用用户事务BEGIN TRANSACTIONCOMMIT来包含多个写操作,减少日志和锁操作。用户定义的事务只包含写操作,读操作仍然是独立的原来的自动提交。如果事务 abort,那么读到那些被事务更改的数据的读操作也将 abort。
    SQLite 不支持嵌套事务。
  • Savepoint
    在用户事务中可以设置 savepoint,savepoint 是数据库那个时刻的所有数据,可用于回滚当事务失败时。
    1
    2
    3
    4
    5
    6
    7
    CREATE TABLE t1(a PRIMARY KEY, b);
    BEGIN;
    INSERT INTO t1 VALUES(1, 'one');
    INSERT INTO t1 VALUES(2, 'two');
    UPDATE t1 SET a = a + 10 //UPDATE操作违反了主键唯一性的约束,那么将产生一个冲突,UPDATE前默认创建了一个savepoint,可以用于回滚。当然也可以显示指定。使用`SAVEPOINT`
    INSERT INTO t1 VALUES(3,null);
    COMMIT;

Lock

数据库的锁是为了保证事务执行的隔离性,通过锁来实现事务访问的顺序性,进而实现了事务的隔离性。
SQLite 的锁是数据库级别的锁,将整个数据库锁住(也就是锁住那个数据库文件,SQLite 将一个库的所有内容存储在一个文件中)。SQLite 为了简化锁的复杂度,采取了严格要求并发程度的方式。它允许同一时间任意数量的读事务。但是同一时间只有一个写事务,没有其他读写事务。
为了实现事务访问的顺序性,需要使用两阶段锁。
Statement 子事务通过所在的父事务获取锁,所有锁持续被事务持有直到事务执行成功或者失败。
SQLite 在 unix 平台上的使用来自于 os_unix.c 文件的实现,但要实现对数据库的操作,其核心在于 SQLite 的锁机制。SQLite 采用粗放型的锁。当一个连接要写数据库时,所有其他的连接被锁住,直到写连接结束了它的事务。SQLite 有一个加锁表,来帮助不同的写数据库者能够在最后一刻再加锁,以保证最大的并发性。SQLite 使用锁逐步上升机制,为了写数据库,连接需要逐步地获得排它锁。对于 5 个不同的锁状态:未加锁(UNLOCKED)、共享(SHARED)、保留(RESERVED)、未决(PENDING)和排它(EXCLUSIVE)。每个数据库连接在同一时刻只能处于其中一个状态。每种状态(未加锁状态除外)都有一种锁与之对应。

  • 锁类型:在 SQLite 中为了写数据库,连接需要逐步地获得排它锁。SQLite 有 5 个不同的锁:未加锁(NO_LOCK)、共享锁(SHARED_LOCK)、保留锁(RESERVED_LOCK)、未决锁(PENDING_LOCK)和排它锁(EXCLUSIVE_LOCK)。

  • NO_LOCK:最初的状态是未加锁状态,在此状态下,连接还没有存取数据库。当连接到了一个数据库,甚至已经用 BEGIN 开始了一个事务时,连接都还处于未加锁状态。

  • SHARED 锁:SHARED 锁意味着进程要读(不写)数据库。一个数据库上可以同时有多个进程获得 SHARED 锁,哪个进程能够在 SHARED_FIRST 区域加共享锁(使用 LockFileEx()LockFileEx()函数),即获得了 SHARED 锁。

  • RESERVED 锁: RESERVED 锁意味着进程将要对数据库进行写操作。一个数据库上同时只能有一个进程拥有 RESERVED 锁。RESERVED 锁可以与 SHARED 锁共存,并可以继续对数据库加新的 SHARED 锁。

  • PENDING 锁:PENDING LOCK 意味着进程已经完成缓存中的数据修改,并想立即将更新写入磁盘。它将等待此时已经存在的读锁事务完成,但是不允许对数据库加新的 SHARED LOCK(这与 RESERVED LOCK 相区别)。

  • EXCLUSIVE 锁:在此锁状态下,进程此时就可以自由地对数据库进行修改了,所有以前对缓冲区所做的修改都会被写到数据库文件。

锁的兼容性和转换

下表中,每一行行首是指当前锁的类型,每一个列首是指申请的锁类型。Y 代表可以,N 代表不行。

每一个事务都需要在执行读写操作前获取合适的锁。这是 Pager 的责任从文件上获取锁。
5 种锁中,pending 是一种内部类型锁。pager 不能直接跟 lock management 申请 pending。pager 只能申请 exclusive,然后 lock management 根据情况,可能返回拒绝或 pending lock 或 exclusive lock。

一次读事务,nolock-shared lock-no lock.
一次写事务,nolock-shared lock-reserved lock- pending lock - Exclusive lock
一次事务回滚,nolock-shared lock-pending lock-exclusive lock

锁的实现

SQLite 依赖 Linux 的的锁原语,fcnt()它可以提供读锁和写锁,并且指定锁定的文件中范围。
文件中的 512 字节用于锁。其中 510 字节用于 shared 锁和 exclusive 锁。fcntl 的读锁这 510 字节,则获取 shared 锁。写锁锁住 510,则获取 exclusive 锁。
reserved 锁,是第 511 字节。使用 fcntl()添加写锁。peding 锁在 512 字节,使用 fcntl()添加写锁。
这里的读锁为什么范围是 510 的原因是,windows 不支持读锁。那么使用一个大的范围,每个 bytes 单独设置一个写锁,可以支持 510 的写锁。那么这些写锁可以作为 sqlite 并发的读锁。

unixLock 方法

os_unix.c 文件

1
static int unixLock(sqlite3_file *id, int eFileLock) //加锁逻辑

面对锁时进程,线程,事务,unixFile,unixInodInfo 之间的关系

Linux 操作系统中。 1.将锁与文件的 inode 联系在一起,而不是使用文件名,因为通过文件可以指向同一个 inode(因为符号链接,硬链接)。 2.锁的获取是通过文件描述符得到的,但是如果文件描述符指向相同的 inode,那么实际的效果还是在操作同一个锁。
因此,Linux 的这种机制,使得在同一个 process 下多个线程或 connection(transction)打开相同的文件时会出现问题,比因为对于系统来说它只按照 process+文件来区分锁的粒度。此时多个线程或 transaction 都在操作同一个锁.
所以,我们需要 unixInodeInfo 结构体在 process 存储文件锁的整体情况。一个 unixInodeInfo 对象代表一个位于 database file 的 SQLite 锁。一个 process 不能拥有多个指向相同文件的 unixInodeInfo 对象。因为一个 process 可以打开多个文件,所以使用 inodeList 来存储所有 unixInodeInfo 对象。

  • unixInodeInfo

os_unix.c 文件
unixInodeInfo结构体,存储了一个进程打开某个文件 iNode 的对应的锁的信息。当进程持有一个文件的 RESERVED 锁,线程如果申请 SHARED 锁,则该进程在 unixInodeInfo 中的 nShared 加 1。如果一个线程申请排它锁,进程则调用 fcntl 获取文件中的排它锁。

1
2
3
4
5
6
7
8
9
10
11
12
struct unixInodeInfo {
struct unixFileId fileId; /* unixInodeInfo 存储在list中,fileId作为key */
int nShared; /* 持有SHARED锁的数量 */
unsigned char eFileLock; /* 进程持有的最高级的锁:SHARED_LOCK, RESERVED_LOCK, NOLOCK, PEDING, EXCLUSIVE. */
unsigned char bProcessLock; /* An exclusive process lock is held */
int nRef; /* 打开的文件数量,因为多个文件可以指向同一个iNode */
unixShmNode *pShmNode; /* Shared memory associated with this inode */
int nLock; /* Number of outstanding file locks */
UnixUnusedFd *pUnused; /* Unused file descriptors to close */
unixInodeInfo *pNext; /* List of all unixInodeInfo objects */
unixInodeInfo *pPrev; /* .... doubly linked */
}
  • unixFile

位于os_unix.c 文件,SQLite 使用 unixFile 来跟踪一个文件的打开实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct unixFile {
sqlite3_io_methods const *pMethod; /* Always the first entry */
sqlite3_vfs *pVfs; /* The VFS that created this unixFile */
unixInodeInfo *pInode; /* Info about locks on this inode */
int h; /* The file descriptor */
unsigned char eFileLock; /* The type of lock held on this fd */
unsigned short int ctrlFlags; /* Behavioral bits. UNIXFILE_* flags */
int lastErrno; /* The unix errno from last I/O error */
void *lockingContext; /* Locking style specific state */
UnixUnusedFd *pUnused; /* Pre-allocated UnixUnusedFd */
const char *zPath; /* Name of the file */
unixShm *pShm; /* Shared memory segment information */
int szChunk;
}
  • 多线程问题

    1. LinuxThreads 是按照线程区分 lock,不同线程之间不能覆盖 lock。NPTL(Native Posix Thread Library)从 3.7.0 开始支持的,同 Process 的线程之间可以相互覆盖 lock。
    2. 当一个线程关闭文件时,Linux 将删除 inode 上所有的锁,而不管具体是哪个线程的锁。SQLite 对此进行了优化,单个线程关闭文件时不会立刻删除所有的 lock,而是跟踪,等到 inode 上最后一个文件描述符被关闭时,才将所有文件描述符关闭并将 lock 释放掉。
  • API

unixLock包含了完整的锁状态转换和加锁。

1
static int unixLock(sqlite3_file *id, int eFileLock) //加锁逻辑

unixFileLock是对 fcntl 和 sqlilte 区域锁实现的封装

1
static int unixFileLock(unixFile *pFile, struct flock *pLock)

unixUnlock是解锁

解开低等级锁,eFileLock 只能是 SHARED_LOCK 或 NO_LOCK。

1
2
3
4
static int unixUnlock(sqlite3_file *id, int eFileLock){
return posixUnlock(id, eFileLock, 0);
}

Page 模块

Page 模块简介

Page 模块的作用是提供了更高层次访问数据库文件的接口,它将原本面向字节的文件操作转换成了面向 page 的文件操作。Tree 模块完全依赖于 page 来访问数据。Page cache 通过将数据放在内存,加快数据的访问,并负责内存和磁盘间的数据同步。同时 Page 模块对事务,日志,锁管理负责。其中锁管理和日志管理在 SQLite 的事务原子提交特性中起着关键作用。
它为每个打开的 database file 维护一个单独的 page cache。如果同一个进程打开 database file 多次,会创建多个 cache 出来。
源码阅读参考:https://blog.csdn.net/pfysw/article/details/79121815

Page 接口

模块提供了 Pager 对象,它跟 database file 一一对应。tree moudle 使用 pager 来调用读写功能。

Pager 的结构中存储,日志文件描述符,数据库文件描述符,日志名,数据库文件名,页缓存,savepoint 数组等信息。

当在用户事务中执行更新操作时是需要 savepoint 的用以失败时的回滚

所有接口函数的定义都在pager.c,函数名都是以 sqlite3Pager 开头

Page Cache

Page caches 位于应用进程的空间中,与操作系统的 cache 无关。每个进程中都保持独立的 Page Object,多个进程打开同一个 database file 的同一个区域,保存多份 page object。一个进程中的多个线程,他们既可以分别独立使用 cache 也可以共享 cache。

  • Cache State
    Pager 结构体中 eState 和 eLock 两个控制 pager 的行为。
    eState 有 7 个状态:PAGE_OPEN(pager 刚刚创建),PAGER_READER(位于读事务中,pager 可以读), PAGER_WRITER_LOCKED(位于写事务中,pager 可以写),PAGER_WRITER_CACHEMOD(位于写事务中且 cache 已被修改),PAGER_WRITER_DBMOD(位于写事务中且开始向 db file 中写),PAGER_WRITER_FINISHED(位于写事务中且已写完 db file,准备提交),PAGER_ERROR(发生读写错误,则进入到 ERROR)。
    eLock 可以使 Pager 位于如下 4 种状态中:NO_LOCK(pager 没有访问数据),SHARED_LOCK(在多个读事务中,对应的多个 pager 正在读),RESERVED_LOCK(pager 已经预定了 database file 但是还没有开始写),EXCLUSIVE_LOCK(在写事务中,pager 已经在写) 中。
    • 一次 pager 的状态转换过程:
      1. 一个 pager 刚开始是 NO_LOCK 状态;
      2. tree 模块调用 sqlite3PagerGet()获取 page,pager 进入到 SHARED_LOCK 状态中;
      3. tree 模块调用 sqlite3PagerUnref()释放所有 page,pager 进入到 NO_LOCK 状态;
      4. tree 模块调用 sqlite3PagerWrite(),pager 进入到 RESERVED_LOCK 状态;
      5. pager 进入到 EXCLUSIVE_LOCK,在真正将 page 修改写入到 database file 前;
      6. 在 sqlite3PagerRollback 或 sqlite3PagerCommit 执行过程中,pager 进入到 NO_LOCK 状态。
  • Cache 组织
    每个 page cache 都通过一个 PCache handler 对象管理,pager 拥有 PCache handler 对象引用。如下是 PCache 对象的一些成员变量。

    为了理解Page的重点内容,这里解释下方的图Pager 对象作为整个 Page 模块的主要对象,Pager 定义了成员PCache *pPCache。PCache 定义了成员sqlite3_pcache *pCache,这里 sqlite3_pcache 是一个可插拔模块,PCache1 实现了这个模块。PCache1 结构体中定义了PgHdr1 *apHashPgHdr1 是对 PgHdr 的具体实现。虚线框带包 page cache 的部分,这部分是可插拔的,接口在 sqlite3_pcache_methods2 中定义,PCache1 实现了这些接口。在 hash 桶的 slot 中分为 PgHdr1 和 slot image 两部分。slot image 持有:PgHdr, a database page image, 一个 private data 用于 btree 保存 page 相关的控制信息。
  • Cache 读
    对 cache 进行读,使用 page number 作为 key 在 hash 表中搜索,如果搜到则返回。如果没有搜到从 disk 中加载到 cache,然后返回。如果加载时 hash table 没有空间了,则需要删除不用的 page 来加载新的 page,或者将脏页刷进 disk 来回收 page。
    返回 page 后,改 page 标记为 pinned,使用后 unpinned。这个标记用于回收 page 时使用。
  • Cache 写
    写时需要先刷 journal log,然后再写。同时写的过程中为 dirty,等待内容被刷入 disk。
  • Cache Fetch
    SQlite 遵循 on command fetch,有些数据库能做到 prefetch。
  • Cache 管理
    基本原则:
    1
    2
    3
    (1) Whenever there is a page in the cache, there is also a master copy of the   page in the database file. Whenever the cache copy is updated, the master copy may need to be updated too.
    (2) For a requested page that is not in the cache the master copy is referenced and a new cache copy is made from the master.
    (3) If the cache is full and a new page is to be placed in the cache, a replacement algorithm is invoked to remove some old page from the cache to make room for the new one.
    关键:cache 的管理是否合理的标准就是 cache 的命中率高不高。
  • Cache 回收机制
    SQLite 使用了类似于 LRU 的机制。
  • 源码阅读
    testpcache 实现了 sqlite3_pcache_methods2 的接口可以认为是 sqlite3_pcache 的具体实现。可以通过阅读此部分源码,来理解 page cache 管理机制。https://blog.csdn.net/pfysw/article/details/79186613,最重要的函数 testpcacheFetch()。pgno 就是作为 hash 表中的 key。

事务管理

pager 通过管理锁和日志来达到管理事务的目的。它负责决定锁类型,锁获取释放的时机。负责决定日志的内容。它通过两阶段协议来管理并发事务的顺序执行。事务管理分为两个方面:正常流程和恢复流程。

  • 正常流程
    包括:读写 page,提交事务和 Statement 子事务,创建和保存 savepoints,回收 page cache 和刷新 page-cache 到 risk。

    1. 读操作
      sqlite3PagerGet()获取或创建 page 在 cache 中。在这个函数中,先获取 shared lock。读取成功后返回指向这个 page cache 的指针。如果是第一次读,这里可以参考 5.8figure,它将 private data 初始化为 0,然后供 tree moudle 调用初始化并存储数据。如何第一次获取锁,在获取 shared lock 时,会判断 hot journal file,如果存在则会先回滚。
      最后,在 hashtable 中为 page 找到一个位置,然后将 disk 内容载入。
    2. 写操作
      sqlite3PagerWrite(),负责 pinned the page。pager 需要获得 reserved lock,然后再升级为 exclusive lock。同时要在日志中写入旧数据并刷入 risk。再写入数据到 cache,最后刷到 risk。如果磁盘扇区大于 page 尺寸,则 SQLite 记录整个扇区而不是 page 到日志中。如果已经将 page 信息 copy 到 journal 文件中,则这个 page 不会有新的记录在 journal 文件中,因为这个 journal file 只负责回滚到事务最初状态。
    3. Cache 刷盘
      在两种情况下刷盘:1.cache 满了;2.事务提交。刷盘时先刷日志再刷数据。
      具体过程参考:https://www.sqlite.org/atomiccommit.html
    4. Commit 操作
      tree moudle 先调用 sqlite3PagerCommitPhaseOne(),获取 exclusive 锁然后调用系统 IO 接口写入数据并通过 fsync 强制刷盘。后调用 sqlite3PagerCommitPhaseTwo(),关闭 journal file,释放锁并降级锁至 NO_LOCK。
      commit 的时间点发生在 journal file 被删除,这样才能保证宕机后不会让旧值覆盖了新值。
      Multidatabase 的情况,需要 VM moudle 来协调,因为 pager 只负责单个数据。这时有个 master journal file 负责记录 single journal file 保证所有单数据的事务能够同步提交或在宕机后回滚。多个数据库的事务之间有可能因为要获取相同的锁,而造成死锁。SQLite 通过 retry 机制来解决。
    5. Statement 操作
      Statement 子事务按照匿名 savepoint 来实现,并且在结束时释放。分为 Read,Write,Commit 三种。Write 的操作有些复杂参看 SQLite Database System:5.4.1.5。
    6. Savepoints
      创建 Savepoints,当一个用户事务创建一个 savepoint 后,SQLite 就进入了 Savepoint 模式。这个模式下 SQLite 不再删除 statment journal 当 Statement 提交时。而是等到释放所有 Savepoint 才释放。
      这里 page 存储的方式不同(没有 Savepoint 时,一个 trasaction 只在 journal 中存储一份 page)。当一个 page 被前一个 Statement 加入了 Statement journal,那么这个 page 可以被当前 Statement 加入同一个 Statement journal。这样同一个 stat journal 拥有一个 page 的多个 log。
      释放 savepoints。当执行 release sp 命令时,PagerSavePoint 对象将被销毁。
  • 恢复过程
    SQLite 需要能够将数据库从错误中恢复,保证数据库的一致性。分为四种情况。

    1. Transaction abort
      当事务持有 RESERVED 或 PENDING lock 时,意味着数据文件没有被改变,所以 pager 删除缓存和 journal 文件即可。当事务持有 EXCLUSIVE lock 时,意味数据文件已经被改变。则需要扫描 journal 文件,恢复数据文件中原本的内容和大小,删除缓存,最后释放 EXCLUSIVE lock。
    2. Statement subtransaction abort
      SQLite 回滚所有日志从 statement journal 和 rollback journal。
    3. Reverting to savepoints
      SQLite 从 Statement journal file 中重放 Savepoint 之后的日志。其中 PagerSavepoint 对象的三个成员很关键:iOffset,iHdrOffset,iSubRec。如果是回滚整个事务,那么只需要回滚 rollback journal 并删除所有 PagerSavepoint 对象。
    4. Recovery from failure
      当系统发生宕机后,如果系统中有残留的 journal file 则意味着写事务在执行时失败了,需要使用 journal file 来回滚。
      整体流程如下:
      1. 获取 SHARED LOCK 在 database file
      2. 检查是否存在 hot journal file,如果没有则不需要恢复,如果有则继续按照如下步骤执行。
      3. 获取 EXCLUSIVE LOCK 在 database file。如果获取失败,则意味着另一个 pager 正在执行恢复。这是他返回 SQLITE_BUSY
      4. 它读取所有日志记录从 rollback journal file 并且回滚。
      5. 回滚后刷新磁盘,让记录存入 disk file。
      6. 删除 journal file
      7. 删除 master journal file 如果需要的话
      8. 将 lock 等级从 EXCLUSIVE 降低到 SHARED

    检测 master journal 过期。如果 mj 中所有 rollback journal 都已经不存在,那么就删除 mj。

  • 其他管理问题

    1. 检查点
      有时为减少故障恢复的时间,一些 database 会定期执行检查点,使得系统出错时可以直接从检查点恢复。SQLite 在 3.7.0 之前的版本并没有检查点。之后才引入检查点。
    2. 空间约束
      一些 database 在回滚事务时产生了新的日志,此时可能会遇到问题,当系统没有更多空间来存储 journal file 时。SQLite 不会有这个问题,因为在回滚事务时,它不产生新的日志。但是空间不够在某种情况下,也会对 SQLite 造成影响。例如一种极端情况:事务执行时删除了一些数据导致 database file 缩小,当要恢复时文件系统的空间被操作系统其他进程占用了,导致无法在回滚时申请到所需要的空间。

Tree 层

Virtual-Machine 层

前端

Virtual-Machine 层

Parser 层

Tokenizer 层

WRK 的结果怎么看?

Latency:表示的是响应时间(需要在命令中添加 –latency),顺序分别是: 平均值,标准偏差,最大值,正负标准差;
其中,平均值,最大值,有一定参考意义,如果标准偏差越小,一定层面能反应待测的接口是比较稳定的

Req/Sec:表示的是每个线程每秒的完成的请求数,顺序分别是: 平均值,标准差,最大值,正负标准差;
参考价值与响应时间类似

链接数和线程数应该设多少?
先上结论:
-t(线程数):
一般是 CPU 核数,最大不要超过 CPUx2 核数,否则会带来额外的上下文切换,将线程数设置为 CPU 核数主要是为了 WRK 能最大化利用 CPU,使结果更准确
-c(连接数):
连接数(connection)可以理解为并发数,一般在测试过程中,这个值需要使用者不断向上调试,直至 QPS 达到一个临界点,便可认为此时的并发数为系统所能承受的最大并发量。
实际上,wrk 会为每个线程分配(c/t)个 socket 连接,每个连接会先执行请求动作,然后等待直到收到响应后才会再发送请求,这个日后会有关于 wrk 的源码解析方便理解,所以每个时间点的并发数大致等于连接数(connection)

连接数(c)与 QPS(q),请求响应时间毫秒(t)的关系大概可理解为:q = 1000/t * c
RTT 为 1ms,如果 c(连接数)为 1,则理论上 QPS 接近 1000,如果 c(连接数)为 100,则 QPS 接近 10w
RTT 为 10ms,如果 c(连接数)为 1,则理论上 QPS 接近 100,如果 c(连接数)为 100,则 QPS 接近 1w

但是服务有自己的负载极限,并发数不能无限放大,这就能解释有的时候连接数越大,反而 QPS 越低,是因为并发数已经设的过高,导致待测系统已经超出自身能承受的负载
https://testerhome.com/topics/22610

shardingsphere-sql-parser 新增语法

给 Oracle 添加语法时,需要注意哪些文件的改动:

实例入门

在安装完 antlr 之后,可以编写简单的 g4 文件

1
2
3
4
grammar Hello;
r:'hello' ID ;
ID:[a-z]+ ;
WS:[\t\r\n]+ -> skip;

运行如下命令

  • antlr4 Hello.g4
  • javac *.java
  • alias grun=‘java org.antlr.v4.runtime.misc.TestRig’ TestRig 是一个调试工具,使用 alias 给它起了一个别名。直接执行grun可以得到帮助信息。
  • grun Hello r -tokens
  • hello parrt
  • EOF

得到如下结果

1
2
3
[@0,0:4='hello',<'hello'>,1:0]
[@1,6:10='parrt',<ID>,1:6]
[@2,12:11='<EOF>',<EOF>,2:0]

解析结果:比如 parrt。@1 表明该词法符号在第 2 个位置,parrt 位于第 6 个到第 10 个位置之间,词法符号类型是 ID,位于输入文本的第 1 行,第 6 个位置处。

调试工具

TestRig 是一个调试工具。
alias grun=‘java org.antlr.v4.runtime.misc.TestRig’ TestRig 是一个调试工具,使用 alias 给它起了一个别名。直接执行grun可以得到帮助信息。

  • 对于Hello语法,执行grun Hello r -tokens可以得到解析的各个 token 的信息。
  • 对于Hello语法,执行grun Hello r -tree可以得到解析的树信息。
  • 对于Hello语法,执行grun Hello r -gui可以得到解析的树的图形信息。
    通过执行 grun,可以查看可以使用的选项。
1
2
3
4
5
6
$ grun
java org.antlr.v4.gui.TestRig GrammarName startRuleName
[-tokens] [-tree] [-gui] [-ps file.ps] [-encoding encodingname]
[-trace] [-diagnostics] [-SLL]
[input-filename(s)]
Use startRuleName='tokens' if GrammarName is a lexer grammar.

-ps files.ps 以 PostScript 格式生成可视化语法分析树,然后将其存储于 file.ps。本章中的语法分析树的图片就是使用-ps 选项生成的。
-encoding encodingname 若当前的区域设定无法正确读取输入,使用这个选项指定测试组件输入文件的编码。例如,在 12.4 节中我们需要通过这个选项来解析日语 XML 文件。
-trace 打印规则的名字以及进入和离开该规则时的词法符号。

  • diagnostics 开启解析过程中的调试信息输出。通常仅在一些罕见情况下使用它产生信息,例如输入的文本有歧义。
  • SLL 使用另外一种更快但是功能稍弱的解析策略。

ANTLR4 的 IntelliJ 插件

打开 g4 文件,选择特定的规则名字,然后右键选择 Test Rule ****。
会在底部弹出框,左侧显示输入的 sql 语句,右侧显示分析的后 tree 以及 Hierarchy,Profiler。

语法分析器

Antlr 依据我们定义的语法规则,产生一个递归下降的语法分析器。下降过程就是从语法分析树的根节点开始,朝着叶节点进行解析的过程。递归下降的语法分析其实际是若干方法的结合,每个方法对应一条规则。递归下降属于自上而下的语法分析器的一种实现。
首先调用的规则,即语义符号的起始点,就会成为语法分析树的根节点。比如:调用上面的 r()方法,作为起始点。
语法分析数的构造过程:识别匹配的规则,将对应规则的方法映射到语法分析树中。
一般规则分为:单一分支,多分支。例如:多个规则 assign,ifstat
单一分支,如:stat: assign;
多分支,如:stat: assign|ifstat;

顺序解析

单一分支,只需要顺序匹配词汇符号。

多分支解析

多分支,需要检查下一个词法符号或者多个词法符号,来决定选择哪个备选分支。这个过程成为预测或语法分析决策。
如果在预测过程中,发现多个分支都匹配的话,则出现了规则的定义出现了歧义,需要解决。

歧义

例如下面这个语法就存在歧义

1
2
3
4
5
6
7
stat: expr ';'
| ID '(' ')' ';'
;

expr: ID '(' ')'
| INT
;

一般情况下要确保语法分析器能够选择唯一匹配的备选分支。不过当存在多个备选分支时,ANTLR 会选取备选分支中的第一条。比如本例中就会选择expr ';'
如下存在词法歧义,begin 是一个关键字,同时也是一个标识符。
词法分析器会匹配最长字符串,如果输入文本 beginner 只会匹配上例中的 ID 这条词法规则。ANTLR 词法分析器不会把它匹配为关键字 BEGIN 后跟着标识符 ner

1
2
BEGIN : 'begin' ;
ID : [a-z]+ ;

语法分析树

构建应用逻辑和语法松耦合的语言类应用程序的关键在于,令语法分析器建立一颗语法分析树,然后在遍历该树的过程中触发应用逻辑代码。
语法分析树的建造过程:词法分析器处理字符序列并将生成的词法符号提供给语法分析器,语法分析器随即根据这些细信息来检查语法的正确性并建造出一颗语法分析树。

1
CharStream->(Lexer)->TokenStream->(Parser)->ParseTree(RuleNode子类+TerminalNode)


ANTLR 使用共享数据结构节约内存,具体办法是:语法分析树中的叶子节点仅仅是盛放词法符号流中的词法符号的容器。每个词法符号都记录了自己在字符序列中的开始位置和结束位置,而非保存子字符串的拷贝。
ParseTree 有两个子类:RuleNode 的子类(非叶子) 和 TerminalNode(叶子)。 RuleNode 的子类包括:StatContext 和 ExprContext.Context 对象知道自己识别的词组中,开始和结束位置处的词法符号,同时提供访问该词组全部元素的方法比如 statContext 类有 ID()和 expr()方法。有了这些方法,我们就可以遍历并操作树中节点。实际上遍历树的机制都是由 ANTLR 生成的代码

语法分析树的访问-监听器、访问器

为了构建一个语言类应用程序,语法分析器需要在遇到特定的输入语句、词组或者词法符号时触发特定的行为。这样的词组->行为的集合构成了我们的语言类应用程序,或者,至少担任了语法和外围程序间接口的角色。
监听器和访问器的区别在于,监听器方法不负责显示调用子节点的访问方法(visit())。访问器必须显式触发对子节点的访问,树的遍历过程才能正常进行,所以访问器可以控制访问的顺序以及节点被访问的次数。

监听器

ANTLR 提供了 ParseTree-Walker 类,自动遍历树然后生成事件并调用监听器。
每个语法文件都会生成一个 ParseTreeLisener 的子类,里面每个规则都有对应的 enter 方法和 exit 方法(),这些方法也称为”事件方法”。这些方法的入参是 ×××Context,提供该方法所需要的所有信息。监听器的操作逻辑在这些 enter 和 exit 方法内添加。下图显示了 ParseTreeWalker 对监听器方法的完整的调用顺序。

访问器

有时候我们希望手动控制遍历数的过程,通过显示的方法调用来访问子节点。在命令行中加入-visitor 选项可以指示 ANTLR 为一个语法生成访问器接口,语法中的每条规则对应接口中的一个 visit 方法。ANTRL 提供了访问器接口和一个默认实现类,这样我们自己只需要覆盖接口中我们感兴趣的方法。

调用方式如下:

1
2
3
ParseTree tree = ...;//语法分析数
MyVisitor v = new MyVisitor();//访问器接口的实现类
v.visit(tree);//visitor访问tree

在事件方法中共享信息

  1. 使用访问器遍历语法分析树,使用访问器方法来返回值。优缺点:无法传递参数,访问器方法只能返回值。

  2. 使用类成员在事件方法之间共享数据,使用栈来模拟返回值。在上下文类中维护一个栈字段,以与 java 调用栈相同的方式,模拟参数和返回值的入和出栈。优缺点:手工操作栈存在失误的可能性,不过栈比较节省空间,所有局部结果的存储在树遍历完成后都会被释放。

  3. 通过对语法分析树的节点进行标注来存储相关数据,通过规则参数和返回值为节点添加字段。在上下文中维护一个 Map 字段,用对应的值来标注节点。优缺点:树标注是我个人的首选解决方案,因为它允许我向事件方法提供任意信息来操纵语法分析树中的各个节点。通过该方案,我可以传递多个任意类型的参数值。在很多情况下,标注比存储转瞬即逝的值的栈更好。使用它,在众多方法中来回传递数据也更不容易事务。这种方案的唯一缺点是,在整个遍历过程中,局部结果都会被保留,因此具有更大的内存消耗。另一方面,某些程序恰好需要标注语法分析树的方案,例如 8.4 节。该程序需要对语法分析树进行多次遍历,将第一趟遍历得到的数据完整地存储在树中是合理的,这样,第二趟遍历就能非常容易地获取这些数据。总之,对数进行标注的方案异常灵活,同时内存占用也处于可接受的范围。

    • 为规则添加返回值
    1
    2
    3
    4
    5
    e returns [int value]
    : e '*' e # Mult
    | e '+' e # Add
    | INT # Int
    ;

    ANTLR 会将所有的参数和返回值放入相关的上下文对象中,这样,value 就成为 EContext 的一个字段。

    1
    2
    3
    4
    public static class AddContext extends ParserRuleContext {
    public int value;
    ...
    }

    在对监听器方法进行实现的时候,就可以按照如下方式存储值

    1
    2
    3
    4
    public void exitAdd(LExprParser.AddContext ctx) {
    // e(0).value 是备选分支中的第一个e子表达式的值
    ctx.value = ctx.e(0).value + ctx.e(1).value; // e '+' e #Add
    }
    • 使用 ParseTreeProperty 类来存储各个节点及对应的值
      定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static class EvaluatorWithProps extends LExprBaseListener {
    /** 使用Map<ParseTree,Inteer>将节点映射到对应的整数值 **/
    ParseTreeProperty<Integer> values = new ParseTreeProperty<Integer>();
    public void setValue(ParseTree node, int value) { values.put(node,value);}
    public int getValue(ParseTree node) {return values.get(node);}
    public void exitAdd(LExprParser.AddContext ctx) {
    int left = getValue(ctx.e(0));
    int right = getValue(ctx.e(1));
    setValue(ctx, left+right);
    }
    }

    调用

    1
    2
    3
    4
    ParseTreeWalker walker = new ParseTreeWalker();
    EvaluatorWithProps evalProp = new EvaluatorWithProps();
    walker.walk(evalProp, tree);
    System.out.println("properties result = "+ evalProp.getValue(tree));
  4. 总结。为获取可复用的语法,我们需要使其与用户自定义的动作分离。这意味着将所有程序自身的逻辑代码放到语法之外的某种监听器或者访问器中。监听器和访问器通过操纵语法分析树来完成工作,ANTLR 会自动生成合适的接口和默认实现类,以便对语法分析树进行遍历。但是,由于事件方法的签名是固定的,无法由程序自行决定,我们找到了三种在事件方法中共享数据的方案。

语法规则

例子 1

文件 ArrayInit.g4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 语法文件通常以grammar关键字开头
* 这是一个名为ArrayInit的语法,它必须和文件名ArrayInit.g4相匹配
*/
grammar ArrayInit;

//一条名为init的规则,它匹配一对花括号中的、逗号分隔的value
init: '{' value (',' value)* ')'; //必须匹配至少一个value。其中*号用来修饰小括号里的内容,表示任意个。
// 一个value可以是嵌套的花括号结构,也可以是一个简单的整数,即INT词法符号
value: init
| INIT
;
//语法分析器的规则必须以小写字母开头,词法分析器的规则必须用大写字母开头
INT: [0-9]+; //定义词法符号INT, 它由一个或多个数字组成
WS: [\t\r\n]+ -> skip; //定义词法规则“空白符号”,丢弃之。-> skip表示跳过丢弃的意思。

ANTLR 对 g4 文件解析后,会生成如下文件:ArrayInitParser.java, ArrayInitLexer.java,ArrayInit.tokens,ArrayInitLexer.tokens,ArrayInitListener.java:ArrayBaseListener.java.
1 ArrayInitParser.java:对应语法 ArrayInit,每条规则对应里面一个方法。
2 ArrayInitLexer.java:用于识别词法规则和文法规则。它是通过 ANTLR 分析词法规则 INT 和 WS,以及语法中的字面值‘{’ ‘}‘ ‘,’生成的。
3 ArrrayInit.tokens: ANTLR 会给每个我们定义的词法符号指定一个数字形式的类型,然后将他们的对应关系存储于该文件中。
4 ArrayInitListener.java 和 ArrayBaseListener.java: 在遍历 AST 时,遍历器能够触发一系列事件,并通知我们提供的监听器对象。ArrayInitListener 接口给出了这些回调方法的定义,ArrayBaseListener 是该接口的默认实现类,为其中的每个方法提供了一个空实现。

ANTLR 语法基本标记

  • 语法包含一系列描述语言结构的规则。这些规则既包括类似 stat 和 expr 的描述语法结构的规则,也包括描述标识符和整数之类的词汇符号的规则
  • 语法分析器的规则以小写字母开头
  • 词法分析器的规则以大写字母开头

语法规则

  • 我们使用|来分隔同一个语言规则的若干备选分支,使用圆括号把一些符号组合成自规则。例如,子规则(‘*‘|’/‘)匹配一个乘法符号或者触发符号。
  • 使用?表达一个或 0 个,使用*表达多个或 0 个,使用+表达至少 1 个。
  • 词法符号依赖,比如(),[],{}等。成对出现的符号。符号使用’’包裹起来。
  • 处理嵌套模式可以使用递归规则处理,规则的定义中包含对自身的调用。有规则直接引用自身称为直接递归,规则间接引用自身称为间接递归。
  • 处理优先级,左结合。ANTLR 在解析输入的语句的时候采用左结合的特性,当处理“1+2*3”的时候,就会先处理加法。这样是有问题的,解决这个问题的办法是在定义语法的时候,乘法的语法写在加法前面,这样,ANTLR 会优先匹配乘法,然后再匹配除法。
  • #标签可以标注备选分支,为每个备选分支单独生成一个监听器方法。如果没有#进行标注,则之后语法规则名对应的一个监听器方法。
1
2
3
4
5
expr: <assign=right> expr '^' expr #Index
| expr '*' expr #Mult
| expr '+' expr #Add
| INT
;

但是,向指数运算这种右结合的情况,就需要使用<assign=right>单独指定。

左递归

  • 左递归规则含义:在某个备选分支的最左侧位置,直接或间接调用了自身。例如:
1
expr: INT|ID|'('expr')'
  • 带有优先级的左递归。在面对 1+23 这样的输入是,根据优先级原则,会按照 1+(23)来处理。
1
2
3
4
5
expr: expr '*' expr
| expr '+' expr
| INT
| ID

  • ANTLR 通过下列四种子表达式运算模式来认定一条规则为左递归规则。
    • 二元。
      1
      2
      3
      4
      expr: ...
      | expr ('<' | '>' | '= ') expr
      ...
      ;
    • 三元
      1
      2
      3
      4
      expr: ...
      | expr '?' expr ':' expr
      ...
      ;
    • 一元前缀
      1
      2
      3
      4
      5
      6
      expr:...
      |'(' type ')' expr
      ...
      |('+'|'-'|'++'|'--') expr
      ...
      ;
    • 一元后缀
      1
      2
      3
      4
      5
      6
      expr:...
      |expr ‘.’ Identifier
      ...
      |expr '.' 'super' '(' exprList? ')'
      ...
      ;

词法规则

  • 匹配标识符ID: [a-zA-Z]+;

  • 匹配数字ID: [0-9]+;

  • 匹配字符串常量。STINRG: '"' .*? '"'。点通配符匹配任意的单个字符。因此,*.就是一个循环,它匹配零个或多个字符组成的任意字符序列。显然,它可以一直匹配到文件结束,但这没有任何意义。为解决这个问题,ANTLR 通过标准正则表达式的标记‘?’,提供了对非贪婪匹配子规则的支持。非贪婪匹配的基本含义是:“获取一些字符,直到发现匹配后续子规则的字符为止”。更准确的描述是,在保证整个父规则完成匹配的前提下,非贪婪子规则匹配数量最少的字符。不过目前这个字符串不支持字符串内包含双引号。

  • 匹配注释并丢弃。COMMENT:'/*' ,*? '*/' -> skip;. skip 是丢弃的含义。

  • 匹配空白字符并丢弃。 WS:[\t\n\r]+ -> skip

ANTLR 解析过程的异常处理

ANTLR 语法分析器能够自动报告语法错误并从错误中恢复继续工作。比如前一个错误的表达式,则输出错误信息。然后继续正确地解析第二个表达式。
ANTLR 的错误处理机制有很高的灵活性。我们可以修改输出的错误信息,捕获识别过程中的异常,甚至改变基本的异常处理策略。

在语法和词法文件中添加动作

定制语法分析

  • 在语法中嵌入任意动作
    我们创建一个构造器,这样我们就能传入希望提取的列号;另外,我们需要在 row 规则的”(…)+”循环中放置一些动作。
    下面的例子,是将输入文本中的指定列显示出来。
    详细的内容,会在第 10 章。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

grammar Rows;

@parser::members { // add members to generated RowsParser
int col;
public RowsParser(TokenStream input, int col) { // custom constructor
this(input);
this.col = col;
}
}

file: (row NL)+ ;

row
locals [int i=0]
: ( STUFF
{
$i++;
if ( $i == col ) System.out.println(\$STUFF.text);
}
)+
;

TAB : '\t' -> skip ; // match but don't pass to the parser
NL : '\r'? '\n' ; // match and pass to the parser
STUFF: ~[\t\r\n]+ ; // match any chars except tab, newline

  • 使用语义判定改变语法分析过程(第 11 章)
    下面的语法用来处理一组整数”2 9 10 3 1 2 3”。第一个数字 2 代表接下来匹配两个数组 9 和 10。紧接着数字 3 告诉我们匹配接下来的三个数字。
    我们的目标是创建一份名为 Data 的语法,将 9 和 10 分为一组,然后 1,2,3 分为一组。
    Data 语法的关键在于一段动作,它的值是布尔类型的,称为一个语义判定:{$i<=$n>}?。它的值在匹配到 n 个输入整数之前保持为 true,其中 n 是 sequence
    语法中的参数。当语义判定的值为 false 时,对应的备选分支就从语法中“消失”了,因此,它也就从生成的语法分析器中“消失”了。在本例中,语义判定的值为 false 使得”(…)*“循环终止,从 sequence 规则返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

grammar Data;

file : group+ ;

group: INT sequence[$INT.int] ;

sequence[int n]
locals [int i = 1;]
: ( {$i<=$n}? INT {\$i++;} )\* // match n integers
;

INT : [0-9]+ ; // match integers
WS : [ \t\n\r]+ -> skip ; // toss out all whitespace

定制词法分析

  • 孤岛语法:处理相同文件中的不同格式
    mode INSIDE 定义模式,INSIDE 模式用于识别 XML 标签,默认模式用于识别标签之外的文本。
    ->定义了动作进入到 INSIDE 模式下
    ->定义了动作 popMode,从 INSIDE 模式下退出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

lexer grammar XMLLexer;

// Default "mode": Everything OUTSIDE of a tag
OPEN : '<' -> pushMode(INSIDE) ;
COMMENT : '<!--' .*? '-->' -> skip ;
EntityRef : '&' [a-z]+ ';' ;
TEXT : ~('<'|'&')+ ; // match any 16 bit char minus < and &

// ----------------- Everything INSIDE of a tag ---------------------
mode INSIDE;

CLOSE : '>' -> popMode ; // back to default mode
SLASH_CLOSE : '/>' -> popMode ;
EQUALS : '=' ;
STRING : '"' ._? '"' ;
SlashName : '/' Name ;
Name : ALPHA (ALPHA|DIGIT)_ ;
S : [ \t\r\n] -> skip ;

fragment
ALPHA : [a-zA-Z] ;

fragment
DIGIT : [0-9] ;

  • 重写输入流
    TokenStreamRewriter 对象可以改写输入的 TokenStream。关键之处,TokenStreamRewriter 对象实际上修改的是词法符号流的“视图”而非词法符号流本身。它认为所有对修改方法的调用都只是一个“指令”,然后将这些修改放入一个队列;在未来词法符号流被重新渲染为文本时,这些修改才会被执行。在每次我们调用 getText()的时候,rewriter 对象都会执行上述队列中的指令。

    这样的方法在源代码插桩或者重构等场合下非常有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public class InsertSerialIDListener extends JavaBaseListener {
TokenStreamRewriter rewriter;
public InsertSerialIDListener(TokenStream tokens) {
rewriter = new TokenStreamRewriter(tokens);
}

@Override
public void enterClassBody(JavaParser.ClassBodyContext ctx) {
String field = "\n\tpublic static final long serialVersionUID = 1L;";
rewriter.insertAfter(ctx.start, field);
}

}

  • 将词法符号送入不同通道
    忽略却保留注释和空白字符的秘诀是将这些词法符号送入一个“隐藏通道”。语法分析器只处理一个通道,因此我们可以将希望保留的词法符号送入其他通道内。
1
2
3
4
5
6
7

COMMENT
: '/_' ._? '_/' -> channel(HIDDEN) // match anything between /_ and \*/
;
WS : [ \r\t\u000C\n]+ -> channel(HIDDEN)
;

将生成的语法分析器与 Java 程序集成,并使用监听器遍历处理

例子 1

Test.java
此例子的目标是将 Java 中,类似{99, 3, 451}的 short 数组翻译成”\u0063\u0003\u01c3”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

import org.antlr.v4.runtime._;
import org.antlr.v4.runtime.tree._;

public class Test{
public static void amin(String[] args) throws Exception{
//新建一个 CharStream,从标准输入读取数据
ANTLRInputStream input = new ANTLRInputStream(System.in);
//新建一个词法分析器,处理输入的 CharStream
ArrayInitLexer lexer = new ArrayInitLexer(input);
//新建一个词法符号的缓冲区,用于存储词法分析器将生成的词法符号
CommonTokenStream tokens = new CommonTokenStream(lexer);
//新建一个语法分析器,处理词法符号缓冲区中的内容
ArrayInitParser parser = new ArrayInitParser(tokens);
ParseTree tree = parser.init(); //针对 init 规则,开始语法分析。因为 init 规则是这些规则中根规则,所以适合从 init 开始分析
/_System.out.println(tree.toStringTree(parser));//用 LISP 风格打印生成的树_/
//新建一个通用的、能够触发回调函数的语法分析树遍历器
ParseTreeWalker walker = new ParseTreeWalker();
//遍历语法分析过程中生成的语法分析树,触发回调
walker.walk(new ShortToUnicodeString(), tree);
System.out.println();//翻译完成后,打印一个\n
}
}

ShortToUnicodeString.java
当遍历 AST 的时候,会对监听器里的方法进行回调。ArrayInitBaseListener 实现了 ArrayInitListener 接口的每个方法,我们只需要继承 ArrayInitBaseListener 并重写自己感兴趣的方法,就可以达到目的。ShortToUnicodeString 中的方法将翻译输入数据的一部分并将结果打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

//将类似{1,2,3}的 short 数组初始化语句翻译为"\u0001\u0002\u0003"
public class ShortToUnicodeString extends ArrayInitBaseListener {
//将{翻译为"
@Override
public void enterInit(ArrayInitParser.InitContext ctx){
System.out.print('"');
}
//将}翻译为"
@Override
public void exitInit(ArrayInitParser.InitContext ctx){
System.out.print('"');
}
//将每个整数翻译为四位的十六进制形式,然后加前缀\u
@Override
public void enterValue(ArrrayInitParser.ValueContext ctx) {
//假定不存在嵌套结构
int value = Integer.valueOf(ctx.INT().getText());//ctx 是 value 的上下文对象,它获取 INT 词法符号对应的整数值。
System.out.printf("\\u%04x", value);//输出 16 进制数,总共占 4 为,前面的空位补 0
}
}

例子 2

LibExpr.g4
此例子用来展示,将语法词法规则分布到多个不同文件中。
使用 import 功能,可以将一个大的语法文件分隔成小的,然后 import 导入连到一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

grammar LibExpr; // Rename to distinguish from original
import CommonLexerRules; // includes all rules from CommonLexerRules.g4
/\*_ The start rule; begin parsing here. _/
prog: stat+ ;

stat: expr NEWLINE
| ID '=' expr NEWLINE
| NEWLINE
;

expr: expr ('\*'|'/') expr //左递归
| expr ('+'|'-') expr
| INT
| ID
| '(' expr ')'
;

CommonLexerRules.g4

1
2
3
4
5
6
7
8

lexer grammar CommonLexerRules; // note "lexer grammar"

ID : [a-zA-Z]+ ; // match identifiers
INT : [0-9]+ ; // match integers
NEWLINE:'\r'? '\n' ; // return newlines to parser (end-statement signal)
WS : [ \t]+ -> skip ; // toss out whitespace. '-> skip'是丢弃操作。

ExprJoyRide.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

import org.antlr.v4.runtime._;
import org.antlr.v4.runtime.tree._;
import java.io.FileInputStream;
import java.io.InputStream;
public class ExprJoyRide {
public static void main(String[] args) throws Exception {
//新建输入流
String inputFile = null;
if ( args.length>0 ) inputFile = args[0];
InputStream is = System.in;
if ( inputFile!=null ) is = new FileInputStream(inputFile);
ANTLRInputStream input = new ANTLRInputStream(is);
//新建词法分析器和语法分析器
ExprLexer lexer = new ExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExprParser parser = new ExprParser(tokens);
//启动语法分析器开始解析
ParseTree tree = parser.prog();
//用文本形式将该规则方法 prog()返回的语法分析树打印出来
System.out.println(tree.toStringTree(parser));
}
}

例子 3 访问器

LabeledExpr.g4
为每个备选分支定义一个标签,标签以#开头。这样 ANTLR 会为每个备选分支生成不同的访问器方法。这样我们就可以对每种输入都获得一个不同的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

grammar LabeledExpr; // rename to distinguish from Expr.g4

prog: stat+ ;

stat: expr NEWLINE # printExpr
| ID '=' expr NEWLINE # assign
| NEWLINE # blank
;

expr: expr op=('_'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| INT # int
| ID # id
| '(' expr ')' # parens
;
//接下来我们为运算符词法符号定义一些名字,这样在随后的访问器中,我们就可以将这些词法符号的名字当做 JAVA 常量来引用。
MUL : '_' ; // assigns token name to '\*' used above in grammar
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
ID : [a-zA-Z]+ ; // match identifiers
INT : [0-9]+ ; // match integers
NEWLINE:'\r'? '\n' ; // return newlines to parser (is end-statement signal)
WS : [ \t]+ -> skip ; // toss out whitespace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public class Calc {
public static void main(String[] args) throws Exception {
String inputFile = null;
if ( args.length>0 ) inputFile = args[0];
InputStream is = System.in;
if ( inputFile!=null ) is = new FileInputStream(inputFile);
ANTLRInputStream input = new ANTLRInputStream(is);
LabeledExprLexer lexer = new LabeledExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
LabeledExprParser parser = new LabeledExprParser(tokens);
ParseTree tree = parser.prog(); // parse
//实例化一个自定义的访问器,然后访问 tree
EvalVisitor eval = new EvalVisitor();
eval.visit(tree);
}
}

使用如下命令可以生成 visitor
antlr4 -no-listener -visitor LabeledExpr.g4
可以生成,LabeledExprBaseVisitor.java,LabeledExprParse.java,LabeledExprLexer.java,LabeledExprVisitor.java。
LabeledExprVisitor 是接口,LabeledExprBaseVisitor 是默认实现类。我们可以继承 LabeledExprBaseVisitor 然后重写里面的函数来实现自定义的逻辑。
EvalVisitor 内重写的方法对应备选分支的标签。里面引用的 JAVA 常量如:MUL,DIV 等。也是在语法文件中定义的词法符号名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

public class EvalVisitor extends LabeledExprBaseVisitor<Integer> {
/\*_ "memory" for our calculator; variable/value pairs go here _/
Map<String, Integer> memory = new HashMap<String, Integer>();

/** ID '=' expr NEWLINE */
@Override
public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
String id = ctx.ID().getText(); // id is left-hand side of '='
int value = visit(ctx.expr()); // compute value of expression on right
memory.put(id, value); // store it in our memory
return value;
}

/** expr NEWLINE */
@Override
public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
Integer value = visit(ctx.expr()); // evaluate the expr child
System.out.println(value); // print the result
return 0; // return dummy value
}

/** INT */
@Override
public Integer visitInt(LabeledExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}

/** ID */
@Override
public Integer visitId(LabeledExprParser.IdContext ctx) {
String id = ctx.ID().getText();
if ( memory.containsKey(id) ) return memory.get(id);
return 0;
}

/** expr op=('*'|'/') expr */
@Override
public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
int left = visit(ctx.expr(0)); // get value of left subexpression
int right = visit(ctx.expr(1)); // get value of right subexpression
if ( ctx.op.getType() == LabeledExprParser.MUL ) return left * right;
return left / right; // must be DIV
}

/** expr op=('+'|'-') expr */
@Override
public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
int left = visit(ctx.expr(0)); // get value of left subexpression
int right = visit(ctx.expr(1)); // get value of right subexpression
if ( ctx.op.getType() == LabeledExprParser.ADD ) return left + right;
return left - right; // must be SUB
}

/** '(' expr ')' */
@Override
public Integer visitParens(LabeledExprParser.ParensContext ctx) {
return visit(ctx.expr()); // return child expr's value
}

}

例子 4

使用监听器来对 java 语法进行分析,根据实现类可以生成对应的接口定义,并保留注释
Java.g4

1
2
3
4
5
6
7
8
9
10
11

classDeclaration
: 'class' Identifier typeParameters? ('extends' type)?
('implements' typeList)?
classBody
;
methodDeclaration
: type Identifier formalParameters ('[' ']')\* methodDeclarationRest
| 'void' Identifier formalParameters methodDeclarationRest
;

ExtractInterfaceListener.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

public class ExtractInterfaceListener extends JavaBaseListener {
JavaParser parser;
public ExtractInterfaceListener(JavaParser parser) {this.parser = parser;}
/\*_ Listen to matches of classDeclaration _/
@Override
public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx){
System.out.println("interface I"+ctx.Identifier()+" {");
}
@Override
public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) {
System.out.println("}");
}

/** Listen to matches of methodDeclaration */
@Override
public void enterMethodDeclaration(
JavaParser.MethodDeclarationContext ctx
)
{
// need parser to get tokens
TokenStream tokens = parser.getTokenStream();
String type = "void";//type为方法的返回类型
if ( ctx.type()!=null ) {
type = tokens.getText(ctx.type());
}
String args = tokens.getText(ctx.formalParameters());//args是入参
System.out.println("\t"+type+" "+ctx.Identifier()+args+";");//ctx.Identifier()是方法名
}

}

ExtractInterfaceTool.java
将自定义的监听器集成到程序里,并遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public class ExtractInterfaceTool {
public static void main(String[] args) throws Exception {
String inputFile = null;
if ( args.length>0 ) inputFile = args[0];
InputStream is = System.in;
if ( inputFile!=null ) {
is = new FileInputStream(inputFile);
}
ANTLRInputStream input = new ANTLRInputStream(is);

JavaLexer lexer = new JavaLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
JavaParser parser = new JavaParser(tokens);
ParseTree tree = parser.compilationUnit(); // parse。开始语法分析的过程

ParseTreeWalker walker = new ParseTreeWalker(); // create standard walker,新建一个标准的遍历器
ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
walker.walk(extractor, tree); // initiate walk of tree with listener,使用监听器初始化对语法分析树的遍历
}

}

错误报告与恢复

入门

ANTLR 在解析语法的时候,可以发现语法和词法的错误并给予提醒,尽管语法有错误,语法分析过程还是照常进行。除了产生良好的错误消息和利用剩余的输入进行重新同步之外,语法分析器还必须能够移动到何时的位置继续语法分析过程。

修改和转发 ANTLR 的错误消息

处理语法错误信息

默认情况下,ANTLR 将所有的错误消息送至标准错误(standard error),不过我们可以通过实现接口 ANTLRErrorListener 来改变这些消息的目标输出和内容。该接口有一个同时应用于词法分析器和语法分析器的 syntaxError()方法。syntaxError()方法接收各式各样的信息无论是错误的位置还是错误的内容。它还接收指向语法分析器的引用,因此我们能够通过引用来查询识别过程的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class VerboseListener extends BaseErrorListener {
@Override
public void syntaxError(Recognizer<?,?> recognizer,
Object offendingSymbol,
int line, int charPositionInLine,
String msg,
RecognitionException e) {
List<String> stack = ((Parser)recognizer).getRuleInvocationStack();
Collections.reverse(stack);
System.err.println("rule stack:"+stack);
System.err.println("line"+line+";"+charPositionInLine+"at"+offendingSymbol+";"+msg);
}
}

使用这种方法,我们的程序就能在语法分析器调用其实规则之前,轻易地为其增加一个错误监听器

1
2
3
4
SimpleParser parser = new SimpleParser(tokens);
parser.removeErrorListeners();
parser.addErrorListener(new VerboseListener());
parser.prog;

在我们增加自定义的错误监听器之前,我们需要移除输出目标是控制台的内置错误监听器,以防出现重复的错误消息。

处理有歧义的语法信息

有时候用户输入的语法可以匹配到我们定义的多个语法分支,这是就存在歧义,默认情况下语法分析器不会通知用户,因为这不是用户的错。而是,我们定义的语法规则的问题。如果希望通知用于,则请使用 addErrorListener()方法添加一个 DiagnosticErrorListener 的实例来告知语法分析器。

1
2
parser.removeErrorListeners();//移除ConsoleErrorListener
parser.addErrorListener(new DiagnosticErrorListener());

此外,你还应当告诉语法分析器,你对所有的歧义告警都感兴趣,而不仅仅是哪些可以快速检测到的。出于效率方面的原因,ANTLR 的决策机制并不是总能发现所有的歧义信息。下面是令语法分析器报告所有歧义的方法:

1
parser.getInterpreter().setPredictionMode(PredictionMode.LL_EXACT_AMBIG_DETECTION);

在开发过程中使用上面提到的诊断错误监听器是个好主意,因为 ANTLR 工具不会对歧义性语法结构提出静态告警。在 ANTLR4 中,只有运行状态的语法分析器才能检测到歧义。这就像是 Java 中静态类型机制和 Python 中动态类型机制的差别。

自动错误恢复机制

错误恢复指的是允许语法分析器在发现语法错误后还能继续的机制。语法分析器在遇到无法匹配词法符号的错误时,执行单词法符号补全和单词法符号移除。如果这些方案不奏效,语法分析器将向后查找词法符号,直到它遇到一个符合当前规则的后续部分的合理词法符号为止,接着,语法分析器将会继续语法分析过程,仿佛什么事情都没有发生过一样。也就是,发生语法错误后从错误中恢复,然后继续语法解析。

  • 通过扫描后续词法符号来恢复。
    当面对真正的非法输入时,当前的规则无法继续下去,此时语法分析器将会向后查找词法符号,知道它认为自己已经完成重新同步时,它就返回原先被调用的规则。我们可以成为‘同步-返回’策略。有人称为“应急模式”,不过它的表现相当好。语法分析器知道自己无法使用当前规则匹配当前输入。它会持续丢弃后续词法符号,知道发现一个可以匹配本规则中断位置之后的某条自规则的词法符号。例如,如果在赋值语句中存在一个语法错误,那么语法分析器的做法就非常合理:丢弃后续的词法符号,直到发现一个分好或者其他的语句终结符为止。这种策略较为激进,但是十分有效。我们下面将要看到,这种基本策略作为后备方案,在启用之前,ANTLR 会试图在规则内部进行恢复。
    重新同步集合是调用栈中所有规则的后续符号集合的并集。
    例如,如果语法分析其分析到 atom 时报错,比如输入的是“[]”,就会在 atom 的时候报错。调用栈为[group,expr,atom]那么此时的后续符号集合则是{‘^’,’]’}。然后语法分析其就从 atom 的错误中恢复了,然后继续分析,分析 expr 发现缺少^从而产生错误,然后继续从符号集合中恢复。然后匹配 group 最后匹配成功。
    恢复过程中,ANTLR 语法分析器会避免输出层叠的错误消息,对于每个语法错误,直到成功从错误中恢复,语法分析器才输出一条错误消息。所以本次语法解析过程,实际上是产生了两处解析错误:atom 和 expr
    注意后续符号集合的生成是动态计算的。是调用栈中所有规则的后续符号的并集。
1
2
3
group : '[' expr ']' | '(' expr ')';
expr: atom '^' INT;
atom: ID | INT;
  • 从不匹配的词法符号中恢复
    在语法分析的过程中,最常见的操作之一就是“匹配词法符号”。对于语法中的每个词法符号 T,语法分析器都会调用 match(T)。如果当前的词法符号不是 T,match()方法就会通知错误监听器,并试图重新同步。为完成同步,它有三种选择:

    • 移除一个词法符号;
    1
    2
    classDef: 'class' ID '{' member+ '}' //a class has one or more members
    {System.out.println("class"+$ID.text);}

    考虑输入文本 class 9 T{int i;},语法分析器会删除 9,然后继续进行同一条规则的语法分析过程–匹配类的定义体。

    • 补全一个词法符号;
    1
    2
    classDef: 'class' ID '{' member+ '}' //a class has one or more members
    {System.out.println("class"+$ID.text);}

    考虑输入文本 class {int i;},语法分析器调用 match(ID),期望发现一个标识符,但实际上发现的却是{。这种情况下,语法分析器知道{是自己所期望的那个词法符号的下一个,因为在 classDef 规则中它位于 ID 之后。此时 match()方法可以假定标识符已经被发现并返回,这样,下一个 match(‘{‘)的调用就会成功。在忽略内嵌动作的前提下,这种方案表现得相当出色。但是,如果词法符号是 null,通过$ID.text 引用了缺失词法符号的打印语句就会引起一个异常。因此,错误处理器会创建一个词法符号,而非简单的假定该词法符号存在,详情参见 DefaultErrorStrategy 中的 getMissingSymbol()方法。新创建的词法符号具有语法分析器所期望的类型,以及和当前词法符号 LA(1)相同的行列位置信息。这个新创建的词法符号阻止了监听器和访问其中引用缺失词法符号时引发的异常。

    • 从子规则的错误中恢复
      在语法中手工插入一条空规则的引用,该规则包含特定的、能够在必要时触发错误恢复的动作。现在,ANTLR4 会在开始处和循环条件判定处自动插入同步检查,以避免集锦的恢复机制。

      • 子规则起始位置。在任意子规则的起始位置,语法分析器会尝试进行单词法符号移除。不过,和词法符号匹配不同的是,语法分析器不会尝试进行单词法符号补全。创建一个词法符号对 ANTLR 来说是很困难的,因为它必须猜测多个备选分支中的哪个会最终胜出。
      • 子规则的循环条件判定位置。如果子规则是一个循环结构,即(…)*或(…)+,在遇到错误时,语法分析器会尝试进行积极的回复,使得自己留在循环内部。在成功地匹配到循环的某个备选分支之后,语法分析器会持续消费词法符号,直到发现满足下列条件之一的词法符号为止:
      • 循环的另一次迭代
      • 紧跟在循环之后的内容
      • 当前规则的重新同步集合中的元素
        例子:
        考虑 Simple 语法的 classDef 规则中的 member+循环结构。如果我们手误多输入了’{‘,member+子规则会在进入 member 之前移除掉多余的那个词法符号。
        1
        2
        3
        4
        5
        > class T {{int i;}
        > EOF
        > line 1:9 extraneous input '{' expecting 'int'
        var i
        class T
    • 捕获失败的语义判定
      语义判定指定了一些必须在运行时为真的条件,以使得语法分析器能够通过这些条件的验证。如果一个判定结果为假,语法分析器会抛出一个 FailedPredicateException 异常,该异常会被当前规则的 catch 语句捕获。语法分析器随机报告一个错误,并运行通用的同步-返回恢复机制。

      1
      2
      3
      4
      5
      vec4: '[' ints[4] ']';
      ints[int max]
      locals [int i=1]
      : INT(',' {$i++;} {$i<=max}? INT)*
      ;

      下列测试给出的整数过多,于是我们看到了一个错误消息,以及错误恢复的过程,在这个过程中,多余的逗号和整数被丢弃了:

      1
      2
      3
      > [1,2,3,4,5,6]
      > EOF
      > line 1:9 rule ints failed predicate: {$i<=max}?

      有时候我们输入的结构语法上是有效的,但是在语义上是无效的,这是,语义判定就不适用了。例如有种语言,要求变量不能被赋值 0.这意味“assignment x =0;”在语法上有效,在语义上无效。
      较好的解决办法是手工输出一个错误,然后令语法分析器按照正确的语法继续继续进行匹配。

      1
      2
      3
      assign
      : ID '=' v=INT {$v.int>0}? ';'
      {if ($v.int==0) notifyListeners("values must be > 0");}
    • 错误恢复机制的防护措施
      ANTLR 的语法分析器具有内置的防护措施,以保证错误回复过程正常结束。如果我们在相同的语法分析位置,遇到了相同的输入情况,语法分析器会在尝试进行恢复之前强制消费一个词法符号。

      1
      2
      3
      4
      5
      6
      7
      > class T {
      > int int x;
      > }
      > EOF
      > line 2:6 no viable alternative at input 'intint'
      > var x
      > class T

      classDef 规则调用了三次 member。其中,第一个 member 没有匹配到任何内容,第二个 member 匹配到了多余的 int。第三次匹配 member 的尝试正确地匹配到了“int x;”序列。

  • 简单地抛出一个异常以启用基本的同步-返回机制
    当面对真正的非法输入时,当前的规则无法继续下去,此时语法分析器将会向后查找词法符号,知道它认为自己已经完成重新同步时,它就返回原先被调用的规则。我们可以成为‘同步-返回’策略。有人称为“应急模式”,不过它的表现相当好。语法分析器知道自己无法使用当前规则匹配当前输入。它会持续丢弃后续词法符号,知道发现一个可以匹配本规则中断位置之后的某条自规则的词法符号。例如,如果在赋值语句中存在一个语法错误,那么语法分析器的做法就非常合适。

勘误备选分支

一些语法错误十分常见,以至于对它们进行特殊处理是值得的。

1
2
3
4
5
6
7
8
9
stat: fcall ';';
fcall
: ID '(' expr ')'
| ID '(' expr ')' ')' {notifyErrorListeners("Too many parentheses");}
| ID '(' expr {notifyErrorListeners("Missing closing");}
;
expr: '(' expr ')'
| INT
;

修改 ANTLR 的错误处理策略

默认的错误处理机制表现出色,不过我们还是会遇到一些非典型的,需要修改默认机制的场景。首先,我们希望关闭某些默认的错误处理功能,他们会带来额外的运行负担。其次,我们可能希望语法分析器在遇到第一个语法错误时就退出。这种情况是,当处理类似 bash 的命令行输入时,从错误中回复是毫无意义的。我们不能一意孤星地执行有风险的命令,因此语法分析器可以一遇到问题就退出。探究错误处理策略,不妨看一下 ANTLRErrorStratege 接口及实现类 DefaultError-Strategy,该类完成了全部的默认错误处理工作。利用 ANTLRErrorListener 和 ANTLRErrorStrategy 接口,我们能够非常灵活地指定错误消息的输出位置、错误消息的内容以及语法分析器从错误中恢复的方法。

在语法分析过程中执行自身的逻辑代码

在之前的学习中,我们的程序逻辑代码都是与语法分析树遍历器分离的,这意味着我们的代码总是在语法分析完成之后执行。在接下来的几节(属性和动作、使用语义判定修改语法分析过程、掌握词法分析的“黑魔法”)中我们可以看到,一些语言类应用程序需要在语法分析的过程中执行自身的逻辑代码。

属性和动作

通常我们应当避免将语法和应用程序的逻辑代码纠缠在一起。不包含动作的语法更容易阅读,不会绑定到特定的目标语言和程序上。尽管如此,内嵌的动作仍然是有用的,原因有如下三个:

  • 简便: 有时,使用少量的动作,避免创建一个监听器或者访问器会使事情变得更加简单。
  • 效率:在资源紧张的程序中,我们可能不想把宝贵的时间和内存浪费在简历语法分析树上。
  • 待判定的语法分析过程:在某些罕见情况下,我们必须依赖从之前的输入流中获取的数据才能正常第进行语法分析过程。一些语法需要建立一个符号表,以便在未来根据情况差异化地识别输入的文本。

动作就是使用目标语言编写的、放置在{}中的任意代码块。我们可以在动作中编写任意代码,只要它们是合法的目标语言语句。动作的典型用法是操纵词法符号和规则引用的属性。例如,我们可以读取一个词法符号对应的文本或者整个规则匹配的文本。通过从词法符号和规则引用中获取的数据,我们就可以打印结果或者执行任意计算。规则允许参数和返回值,因此我们可以在规则之间传递数据。

添加动作

  1. 定义 header 和 members
    这些动作可以位于规则内,也可以位于规则外。header 用于向自动生成的语法分析器和词法分析器中注入 package 或 import 语句。members 用于向自动生成的语法分析器和词法分析器中注入字段(成员变量)和方法(成员函数)。下面显示了注入代码片段的位置。

    1
    2
    3
    4
    5
    <header>
    public class <grammarName>Parser extends Parser {
    <members>
    ...
    }

    我们使用@header{}和@members{}来注入代码。{}内的代码,将出现在语法和词法解析器中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    grammar Expr;
    //定义了语法和词法解析器所属的包,并导入了java.util包
    @header {
    package tools;
    import java.util.*;
    }
    //新增了语法和词法解析器中的字段(成员变量)memory和方法eval()。
    @members {
    Map<String, Integer> memory = new HashMap<String, Integer>();
    int eval (int left, int op, int right) {
    switch (op) {
    case MUL: return left * right;
    case DIV: return left / right;
    case ADD: return left + right;
    case SUB: return left - right;
    }
    return 0;
    }
    }
  2. 在规则中嵌入动作
    动作执行时机是它前面的语法元素之后。本例中,动作出现在备选分支的末尾,因此它们会在语法分析器匹配到整个语句之后被执行。

    1
    2
    3
    4
    stat: e NEWLINE          {System.out.println($e.v);}
    | ID '=' e NEWLINE {memory.put($ID.text, $e.v);}
    | NEWLINE
    ;
    • returns 定义了返回值 v,且类型为 int。通过$e.v 可以引用 e 规则的返回值。
    • a,b,op 都是标记,它们是对=右侧的引用。使用标记可以方便在动作中操作各种值。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      e returns [int v]
      : a=e op=('*'|'/') b=e {$v = eval($a.v, $op.type, $b.v);}
      | a=e op=('+'|'-') b=e {$v = eval($a.v, $op.type, $b.v);}
      | INT {$v = $INT.int;}
      | ID
      {
      String id = $ID.text;
      $v = memory.containsKey(id) ? memory.get(id) : 0;
      }
      | '(' e ')' {$v = $e.v;}
      ;
      ANTLR 通过规则上下文对象来实现语法分析树的节点,每次规则调用都会新建并返回一个规则上下文对象。自然地,规则上下文对象非常适合放置与特定规则相关的数据实体。EContext 的第一部分如下所示:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public static class EContext extends ParserRuleContext{
      public int v; //规则e的返回值
      public EContext a; //规则引用e上的标记a
      public Token op; //类似(‘*’|‘/’)的运算符子规则上的标记
      public EContext b; //规则引用e上的标记b
      public Token INT; //第三个备选分支引用的INT
      public Token ID; //第四个备选分支引用的ID
      public EContext e; //e的调用过程对应的上下文对象的引用
      }

访问词法符号和规则的属性

此节中可以看到如何定义和引用规则的参数和返回值。
原始规则

1
2
3
4
5
6
7
8
9
10
11
12
13
grammar CSV;

file : hdr row+;
hdr : row ;
row : field (',' field)* '\r'? '\n';
field
: TEXT # text
| STRING # string
| # empty
;

TEXT : ~[,\n\r"]+ ;
STRING: '"' ('""'|~'"')* '"' ;

改造后的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* 由规则"file: hdr row+ ;"衍生而来 */
file
locals [int i=0]
: hdr ( rows+=row[$hdr.text.split(",")] {$i++} )+
{
System.out.println($i+"rows");
for (RowContext r : $rows) {
System.out.println("row token interval:"+r.getSourceInterval());
}
}
hdr: row[null] {System.out.println("header: '"+$text.trim()+"'");};
/* 由规则"row: field (',' field)* '\r'? '\n' ;" 衍生而来*/
row[String[] columns] returns [Map<String,String> values]
locals [int col=0]
@init{
$values = new HashMap<String,String>();
}
@after{
if ($values!=null && $values.size()>0) {
System.out.println("values = "+ $values);
}
}
: field
{
if($columns!=null) {
$values.put($columns[$col++].trim(), $field.text.trim());
}
}
(
',' field
{
if ($column!=null){
$values.put($columns[$col++].trim(), $field.text.trim());
}
}
)* '\r'? '\n'
;

输入如下数据

1
2
3
4
User,  Name,   Dept
parrt, Terence, 101
tombu, Tom, 020
bke, Kevin, 008

输出解析结果

1
2
3
4
5
6
7
8
header: 'User, Name, Dept'
values = {Name=Terence, User=parrt, Dept=101}
values = {Name=Tom, User=tombu, Dept=020}
values = {Name=Kevin, User=bke, Dept=008}
3 rows
row token interval:6..11
row token interval:12..17
row token interval:18..23

关键字识别

此节中我们将看到如何使用词法符号属性,text 和 type。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
grammar Keywords;
@lexer::header{
import java.util.*;
}
@lexer::members{
Map<String,Integer> keywords = new HashMap<String,Integer>() {
{
put("begin", KeywordsParser.BEGIN);
put("end", KeywordsParser.END);
put("if", KeywordsParser.IF);
put("then", KeywordsParser.THEN);
put("while", KeywordsParser.WHILE);
}
};
}
stat: BEGIN stat* END
| IF expr THEN stat
| WHILE expr stat
| ID '=' expr ';'
;
expr: INT|CHAR;
ID: [a-zA-Z]+{
if(keywords.containsKey(getText())){
setType(keywords.get(getText()));//重置词法符号类型
}
}
CHAR: '\''.'\''{setText(String.valueOf(getText().charAt(1)))};
tokens {BEGIN, END, IF , THEN, WHILE}

使用语义判定修改语法分析过程

在上节中,内嵌动作仅仅是计算一些值或者打印结果。但是,某些情况下使用内嵌动作类修改语法分析过程是正确识别某些编程语言语句的唯一方案。本章我们学习一种特殊的动作{…}?,成为语义判定,它允许我们在运行时选择性地关闭部分语法。判定本身就是布尔表达式,它会减少语法分析器的在语法分析过程中可选项的数量。适当地减少可选项的数量会增强语法分析器的性能。在词法规则中使用判定,一样会拖慢词法分析器。
语义判定可以在两种常见情况下发挥作用。
第一,我们可能需要语法分析器处理同一门编程语言稍有差异的多个版本。语义判定允许我们通过命令行参数或者其他动态机制,在运行时选择所使用的方言。
第二,应用场景包含处理语法的歧义性。在某些编程语言中,相同的语法结构具有不同的含义,此时判定机制提供了一种方法,让我们能够在对相同输入文本的不同解释中做出选择。

  • 在语法规则中加入判定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    grammar Enum;
    @parser::members {public static boolean java5;}

    prog: (stat
    | enumDecl
    )+
    ;
    stat: id '=' expr ';' {System.out.println($id.text+"="+$expr.text);};
    expr: id | INT;
    enumDecl: {java5}? 'enum' name=id '{' id (',' id)* '}'
    {System.out.println("enum "+$name.text);}
    ;
    ENUM: 'enum';
    ID: [a-zA-Z]+;

    判定可以开启和关闭任何在通过判定后能被匹配的规则。
    下面这样写,一样是正确的。

    1
    prog : ({java5}? enumDecl| stat)+;
  • 在词法规则中加入判定

    1
    2
    3
    4
    5
    enumDecl: 'enum' name=id '{' id (',' id)* '}'
    {System.out.println("enum "+$name.text);}
    ;
    ENUM: 'enum' {java5}? ;//必须放置在ID规则之前
    ID: [a-zA-Z]+;

    需要注意的是,判定出现在词法规则的右侧,而非像文法规则一样的左侧。这是由于在语法分析中,语法分析器会对之后的内容进行预测,因此需要在匹配备选分支之前进行判定。而词法分析器不进行备选分支的预测。它们仅仅寻找最长的匹配文本,然后在发现整个词法符号后做出决策。当 java5 为假时,该判定关闭了 ENUM 规则。当它为真时,ENUM 和 ID 同时匹配了字符序列 e-n-u-m,此时该输入存在歧义。ANTLR 总是通过选择位置靠前的规则来解决词法歧义问题,也就是这里的 ENUM。

  • 识别歧义文本

    1
    2
    3
    4
    5
    6
    /** 前两个备选分支中使用了理想化的判定作为区分这两种情况的Demo **/
    expr: {<<isfunc(ID)>>}? ID '(' expr ')' //一个参数的函数调用
    | {<<istype(ID)>>}? ID '(' expr ')' //构造器风格的对expr的转换
    | INT //整数常量
    | ID //标识符
    ;

    当我们使用这份带判定的语法再次进行测试时,输入”f(i);”被正确地解释成了函数调用表达式,而非声明。输入”T(i);”也被正确解释成了声明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    decl: ID ID
    | {istype()}? ID '(' ID ')'
    ;
    expr: INT
    | ID
    | {!istype()}? ID '(' expr ')'
    ;
    @parser::members {
    Set<String> types = new HashSet<String>() {{add("T");}};
    boolean istype() {return types.contains(getCurrentToken().getText());}
    }

掌握词法分析的“黑魔法”

将词法符号送入不同通道

  • 将空格和回车送入不同通道,而正常的词法符号仍然位于默认的 0 通道。

    1
    2
    WS : [\t\n\r]+ -> channel(1);
    SL_COMMENT : '//' .*? '\n' -> channel(2);
  • 访问隐藏通道
    通过继承监听器,在方法中可以调用 BufferedTokenStream.getHiddenTokensToRight()得到隐藏 channel,进而可以访问 channel 中的 Token 的 text 进行改造。

上下文相关的词法问题

字符流中的孤岛

对 XML 进行语法分析和词法分析

语法参考

语法词汇表

  1. 注释
    ANTLR 支持单行、多行注释。
1
2
3
4
5
grammar T;
/*多行
注释
*/
decl: ID; //单行注释
  1. 标识符
    词法符号名和词法规则名总是以大写字母开头。文法规则总是以小写字母开头。首字母之后的字符可以是大小写字符、数字和下划线。
    tokens{LEXSYMBOL,ASYMBOL,BSYMBOL} //词法符号名
    LEXSYMBOL: [0-9a-z]* //词法规则
1
2
ID,LPAREN,RIGHT_CURLY //词法符号和词法规则名
expr,d2,header_file //文法规则名
ANTLR是支持中文等unicode的。同时也支持非unicode,需要在使用ANTLR工具中使用-encoding选项,以便正确读字符。
  1. 文本常量
    ANTLR 不区分字符常量和字符串常量,所有的文本常量都是由单引号括起来的字符串,如’;’、’if’、‘<’。文本常量不支持正则表达式。文本常量也可以是 Unicode 转义序列’\u00E8’,或者常见转义序列’\n’、’\r’等。
    ANTLR 生成的识别器假定语法中的字符都是 Unicode 字符,ANTLR 运行库根据目标语言对输入文件的编码做出假设,例如对于 Java 运行库假定输入文件为 UTF-8 编码。(Unicode 是字符集每个字符一个码,UTF-8 定义了如何使用二进制存储这个码,比如变长或定长,使用 1 个字节还是多个字节)

  2. 动作
    动作是使用目标语言编写代码块,由花括号包围的任意文本。内嵌代码可以出现在以@header 和@members 命名的动作、词法和文法规则、指定异常捕获区、文法规则的属性区域,以及一些规则元素的选项。

  3. 关键字

    • 不要使用 ANTLR 保留字,ANTLR 语法中的保留字列表:import、tokens、options 等。
    • 不要使用 rule 这个关键字。rule 虽然不是关键字也要避免将它作为规则或者备选分支,因为这样会使得自动生成的 RuleContext 上下文对象与内之类冲突。
    • 不要使用目标语言中的关键字作为词法符号、标签或者规则名。例如,if 规则会生成 if()函数。

语法结构

1
2
3
4
5
6
7
8
9
grammar Name
options{} //可选,options
import...; //可选,import,用来导入词法规则或语法规则
tokens {...} //可选,词法符号名,用于定义未在本语法中定义词法规则的词法符号
@actionName{...} //可选,动作名。 @actionName可以是@header:在生成的代码头部插入{}中的内容; @member在生成的代码中插入变量,变量内容在{}中定义; @after在进行词法或语法分析后,执行{}中定义的动作。

<<rule 1>> //可能混杂一些词法和文法规则
...
<<rule N>>

文法规则

1
2
3
4
5
6
7
8
grammar Name
e : e '*' e # Mult
| e '+' e # Add
| INT # Int
| 'return' value = a ';' #Return //为规则a增加标签value
| '{' el+=INT (, el+=INT)* '}' //通过+=来实现列表标签,就是使用el可以访问一组INT数据
;

1.#标签的作用是为每个备选分支都生成一个监听器。不然的话,就只为规则生成监听器。所以标签的名要求不能与已有标签名或者规则名冲突。
2.ANTLR 为每个规则生成规则上下文对象,并附带访问方法。NameContext 对象里面包含访问 Econtext 的方法。
3.为规则 a 增加标签 value
4.规则元素

动作和属性

词法规则

通配符和非贪婪子规则

语义判定

前言

在 Java 程序的运行过程中,对 JVM 和系统的监测一直是 Java 开发人员在开发过程所需要的。一直以来,Java 开发人员必须通过一些底层的 JVM API,比如 JVMPI 和 JVMTI 等,才能监测 Java 程序运行过程中的 JVM 和系统的一系列情况,这种方式一直以来被人所诟病,因为这需要大量的 C 程序和 JNI 调用,开发效率十分低下。于是出现了各种不同的专门做资源管理的程序包。为了解决这个问题,Sun 公司也在其 Java SE 5 版本中,正式提出了 Java 管理扩展(Java Management Extensions,JMX)用来管理检测 Java 程序(同时 JMX 也在 J2EE 1.4 中被发布)。

JMX 的提出,让 JDK 中开发自检测程序成为可能,也提供了大量轻量级的检测 JVM 和运行中对象 / 线程的方式,从而提高了 Java 语言自己的管理监测能力。

JMX 和系统管理

管理系统(Management System)
要了解 JMX,我们就必须对当前的 IT 管理系统有一个初步的了解。随着企业 IT 规模的不断增长,IT 资源(IT resource)数量不断增加,IT 资源的分布也越来越分散。可以想象,甚至对于一家只有几百台 PC 公司的 IT 管理人员来说,分发一个安全补丁并且保证其在每台 PC 上的安装,如果只依赖人工来完成那简直就是一场噩梦。这样,IT 管理系统就应运而生。

然而,CPU、网卡、存储阵列是 IT 资源;OS、MS Office、Oracle database、IBM Websphere 也是 IT 资源。IT 管理系统若要对这些 IT 资源进行管理,就必须对这些管理对象有所了解:形形色色的 IT 资源就像是说着不同语言的人:Oralce 数据库表达内存紧张的方式和 Window XP 是绝然不同的, 而 IT 管理系统就像建造通天塔的经理,必须精通所有的语言, 这几乎是一个不可能完成的任务。难道 IT 管理系统是另外一个通天塔吗?当然不是!其实我们只要给每个 IT 资源配个翻译就可以了。

管理系统的构架


图 1. 管理系统构架

上图分析了管理系统的基本构架模式。其中 Agent / SubAgent 起到的就是翻译的作用:把 IT 资源报告的消息以管理系统能理解的方式传送出去。

也许读者有会问,为什么需要 Agent 和 SubAgent 两层体系呢?这里有两个现实的原因:

管理系统一般是一个中央控制的控制软件,而 SubAgent 直接监控一些资源,往往和这些资源分布在同一物理位置。当这些 SubAgent 把状态信息传输到管理系统或者传达管理系统的控制指令的时候,需要提供一些网络传输的功能。
管理系统的消息是有一定规范的,消息的翻译本身是件复杂而枯燥的事情。
一般来说,管理系统会将同一物理分布或者功能类似的 SubAgent 分组成一组,由一个共用的 Agent 加以管理。在这个 Agent 里封装了 1 和 2 的功能。

JMX 和管理系统

JMX 既是 Java 管理系统的一个标准,一个规范,也是一个接口,一个框架。图 2 展示了 JMX 的基本架构。


图 2. JMX 构架

和其它的资源系统一样,JMX 是管理系统和资源之间的一个接口,它定义了管理系统和资源之间交互的标准。 javax.management.MBeanServer 实现了 Agent 的功能,以标准的方式给出了管理系统访问 JMX 框架的接口。而 javax.management.MBeans 实现了 SubAgent 的功能,以标准的方式给出了 JMX 框架访问资源的接口。而从类库的层次上看,JMX 包括了核心类库 java.lang.management 和 javax.management 包。 java.lang.management 包提供了基本的 VM 监控功能,而 javax.management 包则向用户提供了扩展功能。

JMX 的基本框架

JMX 使用了 Java Bean 模式来传递信息。一般说来,JMX 使用有名的 MBean,其内部包含了数据信息,这些信息可能是:应用程序配置信息、模块信息、系统信息、统计信息等。另外,MBean 也可以设立可读写的属性、直接操作某些函数甚至启动 MBean 可发送的 notification 等。MBean 包括 Standard,MXBean,Dynamic,Model,Open 等几种分类,其中最简单是标准 MBean 和 MXBean,而我们使用得最多的也是这两种。MXBean 主要是 java.lang.management 使用较多,将在下一节中介绍。我们先了解其他一些重要的 MBean 的种类。

标准 MBean

标准 MBean 是最简单的一类 MBean,与动态 Bean 不同,它并不实现 javax.management 包中的特殊的接口。说它是标准 MBean, 是因为其向外部公开其接口的方法和普通的 Java Bean 相同,是通过 lexical,或者说 coding convention 进行的。下面我们就用一个例子来展现,如何实现一个标准 MBean 来监控某个服务器 ServerImpl 状态的。ServerImpl 代表了用来演示的某个 Server 的实现:

1
2
3
4
5
6
7
package standardbeans;
public class ServerImpl {
public final long startTime;
public ServerImpl() {
startTime = System.currentTimeMillis();
}
}

显示更多
然后,我们打算使用一个标准 MBean,ServerMonitor 来监控 ServerImpl:

1
2
3
4
5
6
7
8
9
10
package standardbeans;
public class ServerMonitor implements ServerMonitorMBean {
private final ServerImpl target;
public ServerMonitor(ServerImpl target){
this.target = target;
}
public long getUpTime(){
return System.currentTimeMillis() - target.startTime;
}
}

显示更多
这里的 ServerMonitorBean 又是怎么回事呢? MXBean 规定了标准 MBean 也要实现一个接口,所有向外界公开的方法都要在这个接口中声明。否则,管理系统就不能从中获得相应的信息。此外,该接口的名字也有一定的规范:即在标准 MBean 类名之后加上”MBean”后缀。若 MBean 的类名叫做 MBeansName 的话,对应的接口就要叫做 MBeansNameMBean。

对于管理系统来说,这些在 MBean 中公开的方法,最终会被 JMX 转化成属性(Attribute)、监听(Listener)和调用(Invoke)的概念。如果读者对 Java Bean 有一些了解的话,不难看出, public long getUpTime() 对应了 Bean 中的一个称为”upTime”的只读属性。

下面我们就看一个模拟管理系统的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package standardbeans;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
public class Main {
private static ObjectName objectName ;
private static MBeanServer mBeanServer;
public static void main(String[] args) throws Exception{
init();
manage();
}
private static void init() throws Exception{
ServerImpl serverImpl = new ServerImpl();
ServerMonitor serverMonitor = new ServerMonitor(serverImpl);
mBeanServer = MBeanServerFactory.createMBeanServer();
objectName = new ObjectName("objectName:id=ServerMonitor1");
mBeanServer.registerMBean(serverMonitor,objectName);
}
private static void manage() throws Exception{
Long upTime = (Long) mBeanServer.getAttribute(objectName,
"upTime");
System.out.println(upTime);
}
}

显示较少
JMX 的核心是 MBServer。Java SE 已经提供了一个默认实现,可以通过 MBServerFactory.createMBeanServer() 获得。每个资源监控者(MBean)一般都会有名称(ObjectName), 登记在 MBServer 内部的一个 Repository 中。注意,这个 ObjectName 对于每一个 MBServer 必须是唯一的,只能对应于一个 MBean。(读者有兴趣的话,可以试着再给 mBeanServer 注册一个同名的 objectName,看看会怎么样。) 上述例子是在 init() 方法中完成向 MBeanServer 注册工作的。

在管理过程中,管理系统并不与资源或者 SubAgent 直接打交道,也就是说,这里不会直接引用到 MBean。而是通过 MBeanServer 的 getAttribute 方法取得对应 MBean 的属性的。

动态 MBean

虚拟机检测

JMX 与虚拟机检测
JMX 的提出,为 Java 虚拟机提供了 Java 层上的检测机制。J2SE 中,新提出的 java.lang.management 包即是 JMX 在 JDK 的一个应用,它提供了大量的有用的接口,通过 MBean 方式,提供了对 Java 虚拟机和运行时远端的监控和检测方式,来帮助用户来检测本地或者远端的虚拟机的运行情况。有了 JMX 之后,我们可以设计一个客户端,来检测远端一个正在运行的虚拟机中的线程数、线程当前的 Stack、内存管理、GC 所占用的时间、虚拟机中的对象和当前虚拟机参数等重要的参数和运行时信息。JMX 另外的一个重要功能是对配置信息的检测和再配置。比如,我们可以在远端查看和修改当前 JVM 的 verbose 参数,以达到动态管理的目的。甚至,我们可以在远端指挥 JVM 做一次 GC,这在下文中有详细介绍。

JMX 提供的虚拟机检测 API

检测虚拟机当前的状态总是 Java 开放人员所关心的,也正是因为如此,出现了大量的 profiler 工具来检测当前的虚拟机状态。从 Java SE 5 之后,在 JDK 中,我们有了一些 Java 的虚拟机检测 API,即 java.lang.management 包。Management 包里面包括了许多 MXBean 的接口类和 LockInfo、MemoryUsage、MonitorInfo 和 ThreadInfo 等类。从名字可以看出,该包提供了虚拟机内存分配、垃圾收集(GC)情况、操作系统层、线程调度和共享锁,甚至编译情况的检测机制。这样一来,Java 的开发人员就可以很简单地为自己做一些轻量级的系统检测,来确定当前程序的各种状态,以便随时调整。

要获得这些信息,我们首先通过 java.lang.management.ManagementFactory 这个工厂类来获得一系列的 MXBean。包括:

  • ClassLoadingMXBean

ClassLoadMXBean 包括一些类的装载信息,比如有多少类已经装载 / 卸载(unloaded),虚拟机类装载的 verbose 选项(即命令行中的 Java – verbose:class 选项)是否打开,还可以帮助用户打开 / 关闭该选项。

  • CompilationMXBean

CompilationMXBean 帮助用户了解当前的编译器和编译情况,该 mxbean 提供的信息不多。

  • GarbageCollectorMXBean

相对于开放人员对 GC 的关注程度来说,该 mxbean 提供的信息十分有限,仅仅提供了 GC 的次数和 GC 花费总时间的近似值。但是这个包中还提供了三个的内存管理检测类:MemoryManagerMXBean,MemoryMXBean 和 MemoryPoolMXBean。

  • MemoryManagerMXBean

这个类相对简单,提供了内存管理类和内存池(memory pool)的名字信息。

  • MemoryMXBean

这个类提供了整个虚拟机中内存的使用情况,包括 Java 堆(heap)和非 Java 堆所占用的内存,提供当前等待 finalize 的对象数量,它甚至可以做 gc(实际上是调用 System.gc)。

  • MemoryPoolMXBean

该信息提供了大量的信息。在 JVM 中,可能有几个内存池,因此有对应的内存池信息,因此,在工厂类中,getMemoryPoolMXBean() 得到是一个 MemoryPoolMXBean 的 list。每一个 MemoryPoolMXBean 都包含了该内存池的详细信息,如是否可用、当前已使用内存 / 最大使用内存值、以及设置最大内存值等等。

  • OperatingSystemMXBean

该类提供的是操作系统的简单信息,如构架名称、当前 CPU 数、最近系统负载等。

  • RuntimeMXBean

运行时信息包括当前虚拟机的名称、提供商、版本号,以及 classpath、bootclasspath 和系统参数等等。

  • ThreadMXBean

在 Java 这个多线程的系统中,对线程的监控是相当重要的。ThreadMXBean 就是起到这个作用。ThreadMXBean 可以提供的信息包括各个线程的各种状态,CPU 占用情况,以及整个系统中的线程状况。从 ThreadMXBean 可以得到某一个线程的 ThreadInfo 对象。这个对象中则包含了这个线程的所有信息。

java.lang.management 和虚拟机的关系

我们知道,management 和底层虚拟机的关系是非常紧密的。其实,有一些的是直接依靠虚拟机提供的公开 API 实现的,比如 JVMTI;而另外一些则不然,很大一块都是由虚拟机底层提供某些不公开的 API / Native Code 提供的。这样的设计方式,保证了 management 包可以提供足够的信息,并且使这些信息的提供又有足够的效率;也使 management 包和底层的联系非常紧密。

锁检测

我们知道,同步是 Java 语言很重要的一个特性。在 Java SE 中,最主要的同步机制是依靠 synchronize 关键字对某一个对象加锁实现的;在 Java SE 5 之后的版本中,concurrent 包的加入,大大强化了 Java 语言的同步能力,concurrent 提供了很多不同类型的锁机制可供扩展。因此,要更好地观测当前的虚拟机状况和不同线程的运行态,去观察虚拟机中的各种锁,以及线程与锁的关系是非常必要的。很可惜的是,在过去的 JDK 中,我们并没有非常方便的 API 以供使用。一个比较直接的检测方式是查看线程的 stack trace,更为强大全面(但是也更复杂并且效率低下)的方案是得到一个 VM 所有对象的快照并查找之,这些策略的代价都比较大,而且往往需要编写复杂的 Native 代码。

JDK 6 里提供了一些相当简单的 API 来提供这个服务。首先了解两个新类,LockInfo 和 MonitorInfo 这两个类承载了锁的信息。LockInfo 可以是任何的 Java 锁,包括简单 Java 锁和 java.util.concurrent 包中所使用的锁(包括 AbstractOwnableSynchronizer 和 Condition 的实现类 / 子类),而 MonitorInfo 是简单的 Java 对象所代表的锁。要检测一个线程所拥有的锁和等待的锁,首先,要得到一个线程的 ThreadInfo,然后可以简单地调用:

getLockedMonitors()

返回一个所有当前线程已经掌握的锁对象的列表。

getLockedSynchronizers()

对于使用 concurrent 包的线程,返回一个该线程所掌握的”ownable synchronizer”(即 AbstractOwnableSynchronizer 及其子类)所组成的列表。

getLockInfo()

当前线程正在等待的那个锁对象的信息就可以知道线程所有的锁信息。通过这些锁信息,我们很方便的可以知道当前虚拟机的所有线程的锁信息。由此,我们还可以推导出更多的信息。

死锁检测

死锁检测一直以来是软件工程师所重视的,显然一个死锁的系统永远是工程师最大的梦魇。Java 程序的死锁检测也一直以来是 Java 程序员所头痛的。为了解决线程间死锁问题,一般都有预防(代码实现阶段)和死锁后恢复(运行时)两种方式。以前 Java 程序员都重视前者,因为在运行态再来检测和恢复系统是相当麻烦的,缺少许多必要的信息;但是,对于一些比较复杂的系统,采取后者或者运行时调试死锁信息也是非常重要的。由上面所说,现在我们已经可以知道每一个线程所拥有和等待的锁,因此要计算出当前系统中是否有死锁的线程也是可行的了。当然,Java 6 里面也提供了一个 API 来完成这个功能,即:

ThreadMXBean.findDeadlockedThreads()

这个函数的功能就是检测出当前系统中已经死锁的线程。当然,这个功能复杂,因此比较费时。基本上仅仅将之用于调试,以便对复杂系统线程调用的改进。

BNF

巴科斯范式(BNF: Backus-Naur Form 的缩写)是由 John Backus 和 Peter Naur 首先引入的用来描述计算机语言语法的符号集。现在,几乎每一位新编程语言书籍的作者都使用巴科斯范式来定义编程语言的语法规则。

在 BNF 中,双引号中的字(“word”)代表着这些字符本身。而 double_quote 用来代表双引号。

在双引号外的字(有可能有下划线)代表着语法部分。

< > : 内包含的为必选项。
  [ ] : 内包含的为可选项。
  { } : 内包含的为可重复 0 至无数次的项。
  | : 表示在其左右两边任选一项,相当于”OR”的意思。
  ::= : 是“被定义为”的意思
  “…” : 术语符号
  […] : 选项,最多出现一次
  {…} : 重复项,任意次数,包括 0 次
  (…) : 分组
  | : 并列选项,只能选一个
  斜体字: 参数,在其它地方有解释

下面是是用 BNF 来定义的 Java 语言中的 For 语句的实例:

1
2
3
4
5
6
FOR_STATEMENT ::=
"for" "(" ( variable_declaration |
( expression ";" ) | ";" )
[ expression ] ";"
[ expression ] ";"
")" statement

EBNF

扩展巴科斯-瑙尔范式(Extended Backus–Naur Form,EBNF)是一种用于描述计算机编程语言等正式语言的与上下文无关语法的元语法(metasyntax)符号表示法。简而言之,它是一种描述语言的语言。它是基本巴科斯范式(BNF)元语法符号表示法的一种扩展。

最初由尼克劳斯·维尔特开发,最常用的 EBNF 变体由标准是 ISO-14977 所定义。

EBNF 的基本语法形式如下,这个形式也被叫做 production:

1
左式(LeftHandSide) = 右式(RightHandSide).

左式也被叫做 非终端符号(non-terminal symbol),而右式则描述了其的组成。

终端符号与非终端符号

  • 终端符号(Terminal symbols):形成所描述的语言的最基本符号。所描述语言的标点符号(不是 EBNF 自己的)会被左右加引号(它们也是终端符号),而其他终端符号会用粗体(这边因不方便加粗,就不加粗了)打印。
  • 非终端符号:是用于描述语法的变量,它必须被定义在一个 production 中。或说,它们必须出现在某个地方的 production 的左式中。

符号

约定

1 .使用了如下约定:

  • EBNF 的每个元标识符(meta-identifier)都被写为用连字符(“-“,hyphens)连接起来的一个或多个单词;
  • 以 “-symbol” 结束的元标识符是 EBNF 的终端符号。

2 .用普通字符表示的 EBNF 操作符按照优先级(顶部为最高优先级)排序为:

1
2
3
4
5
6
7
*repetition-symbol(重复符)
-except-symbol(除去符)
, concatenate-symbol(连接符)
| definition-separator-symbol
= defining-symbol(定义符)
; terminator-symbol(结束符)
. terminator-symbol(结束符)

3 .以下的括号对(bracket pairs)能够改变优先级,括号对间也有优先级(顶部为最高优先级):

1
2
3
4
5
6
7
'  first-quote-symbol            first-quote-symbol  '    (* 引用 *)
" second-quote-symbol second-quote-symbol " (* 引用 *)
(* start-comment-symbol end-comment-symbol *) (* 注释 *)
( start-group-symbol end-group-symbol ) (* 分组 *)
[ start-option-symbol end-option-symbol ] (* 可选 *)
{ start-repeat-symbol end-repeat-symbol } (* 重复 *)
? special-sequence-symbol special-sequence-symbol ? (* 特殊序列 *)

下例示范了怎么表达重复:

1
2
3
4
5
6
7
aa = "A";
bb = 3 * aa, "B";
cc = 3 * [aa], "C";
dd = {aa}, "D";
ee = aa, {aa}, "E";
ff = 3 * aa, 3 * [aa], "F";
gg = {3 * aa}, "D";

这些规则定义的终端字符串如下:

1
2
3
4
5
6
7
aa: A
bb: AAAB
cc: C AC AAC AAAC
dd: D AD AAD AAAD AAAAD etc.
ee: AE AAE AAAE AAAAE AAAAAE etc.
ff: AAAF AAAAF AAAAAF AAAAAAF
gg: D AAAD AAAAAAD etc.

扩展

除了标准的定义,在 FREESCALE 文档中还使用了以下约定:

  • 计数重复:任何由”{“和”}”括起来并后跟一个上标 x 的东西必须准确地重复出现 x 次。x 也可能是一个非终端字符。比如下例中,Stars 相当于四个星号:
    Stars = {“*”}4.
  • 字节数:见到任何紧跟着由一对中括号“[”和“]”括起来的数字 n 的标识符,都应该认为它是一个高位字节在前的二进制数,并且字节数为 n,如:
    Struct=RefNo FilePos[4].
  • 在一些例子中,我们会使用”<”和”>”括起来一些文本。这些文本是 元文本(meta–literal),它们的位置应该被它们所描述的东西替代掉,如,对于 < any char >,它的位置可以插入任意字符。

示例

以下提供一些示例以直观的理解 EBNF。

1
2
3
4
digit excluding zero = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
digit = "0" | digit excluding zero ;
natural number = digit excluding zero, { digit } ;
integer = "0" | [ "-" ], natural number ;

digit excluding zero 可以是 1 到 9 任意一个字符,digit 则扩展为 0 到 9 任意一个字符。
natural number 可以是 1、2、…、10、…、12345、…,因为{}代表重复任意次,包括 0 次。
integer 则可以是 0 或者可能带个负号的自然数。

这是用 EBNF 描述的 EBNF 自身语法:

1
2
3
4
5
6
7
8
9
10
Production     = NonTerminal "=" Expression ".".
Expression = Term {"|" Term}.
Term = Factor {Factor}.
Factor = NonTerminal
| Terminal
| "(" Expression ")"
| "[" Expression "]"
| "{" Expression "}".
Terminal = Identifier | “"“ <any char> “"“.
NonTerminal = Identifier.

非终端符号可以是任意你喜欢的名字,而终端符号则要不然是出现在被描述的语言中的标识符,要不然就是任何被引号括起来的字符序列。
然后 Factor(参数)可以是终端字符、非终端字符、三种括号中任意一种括起来的表达式。
Term(术语)由起码一个 Factor 组合而成……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(* a simple program syntax in EBNF − Wikipedia *)
program = 'PROGRAM', white space, identifier, white space,
'BEGIN', white space,
{ assignment, ";", white space },
'END.' ;
identifier = alphabetic character, { alphabetic character | digit } ;
number = [ "-" ], digit, { digit } ;
string = '"' , { all characters - '"' }, '"' ;
assignment = identifier , ":=" , ( number | identifier | string ) ;
alphabetic character = "A" | "B" | "C" | "D" | "E" | "F" | "G"
| "H" | "I" | "J" | "K" | "L" | "M" | "N"
| "O" | "P" | "Q" | "R" | "S" | "T" | "U"
| "V" | "W" | "X" | "Y" | "Z" ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
white space = ? white space characters ? ;
all characters = ? all visible characters ? ;

对应的语法正确的程序如下:

1
2
3
4
5
6
7
8
9
10
PROGRAM DEMO1
BEGIN
A:=3;
B:=45;
H:=-100023;
C:=A;
D123:=B34A;
BABOON:=GIRAFFE;
TEXT:="Hello world!";
END.

常用命令

  • 查看 pod:kubectl get pod -n kubectl describe pod -n
  • 查看 pod 运行在哪个 node 节点:kubectl get pod -o wide -n
  • 查看 pod 有哪些 label:kubectl get pod –show-labels -n
  • 查看 node 有哪些 label:kubectl get nodes –show-labels
  • 查看 service:kubectl get svc -n
  • 进入容器的交互式 shell:kubectl exec -it - /bin/bash -n
  • 查看日志:kubectl logs -n
  • 查看后十行日志:kubectl logs –tail=10
  • 查看上次启动的日志:kubectl logs -p
  • 修改 image 版本:kubectl set image deployment nginx(deploy name) nginx(container name)=nginx:1.9.1
  • 修改副本数:kubectl scale –replicas=3 deployment mysql
  • 拷贝文件:kubectl cp mysql-478535978-1dnm2:/tmp/message.log message.log
  • 查看 yaml 文件:cd /etc/kubernetes/apps/
  • 应用 yaml 文件:kubectl apply -f xxx.yml

关键命令

  • 驱逐 node 上所有 pod:kubectl taint node key1=value1:NoExecute
  • 驱逐 node 上 pod,不驱逐 ds:kubectl drain –delete-local-data –force –ignore-daemonsets
  • 恢复调度:kubectl uncordon
  • 强制删除 namespace:kubectl delete namespace NAMESPACENAME –force –grace-period=0
  • 命令行方式重启 Deployment:kubectl patch deployment my-app –patch ‘{“spec”: {“template”: {“metadata”: {“annotations”: {“version/config”: “20180411” }}}}}’
  • 查看某个资源的字段意义:kubectl explain deployment.spec.template.spec.dnsPolicy
  • 查看资源类型及版本:kubectl api-resources kubectl api-versions
  • 创建一个测试的 deployment:kubectl run curl -it –image=pstauffer/curl 或 kubectl run curl -it –image=registry.sensetime.com/viper-test/curl
  • 创建一个 nginx 的 deployment 并暴露服务:kubectl run nginx –image=nginx –expose –port=80
  • 清理 docker 镜像:docker system prune -f 命令可以用于清理磁盘,删除关闭的容器、无用的数据卷和网络,以及 dangling 镜像(即无 tag 的镜像);docker system prune -af 命令清理得更加彻底,可以将没有容器使用 Docker 镜像都删掉。注意,这两个命令会把你暂时关闭的容器,以及暂时没有用到的 Docker 镜像都删掉。

配置

deployment.yml

1

service.yml

1

ingress.yml

1

serviceMonitor.yml

1

jdk8 源码解析之 sql 包:JDBC 源码解析

参考连接:https://blog.csdn.net/mxy88888/article/details/94315198

在开发项目时我们经常会需要与数据库进行交互,为了统一标准,在 java jdk 中提供了一组与数据库交互的 api(java.sql.*),每个厂商通过继承实现 sql 包下的接口和类完成与数据库交互的工作(例如 mysql-connector-java)。以 mysql 为例:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] arg) throws Exception {


Class.forName("com.mysql.jdbc.Driver");
Connection connection= DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
PreparedStatement prepareStatement=connection.prepareStatement("select * from student");
ResultSet resultSet=prepareStatement.executeQuery();
while(resultSet.next()){
System.out.println(resultSet.getString("id")+":"+resultSet.getString("studname"));
}
connection.close();
}

JDBC (Java Database Connectivity) API,即 Java 数据库编程接口,是一组标准的 Java 语言中的接口和类,使用这些接口和类,Java 客户端程序可以访问各种不同类型的数据库。比如建立数据库连接、执行 SQL 语句进行数据的存取操作。

JDBC 规范采用接口和实现分离的思想设计了 Java 数据库编程的框架。接口包含在 java.sql 及 javax.sql 包中,其中 java.sql 属于 JavaSE,javax.sql 属于 JavaEE。这些接口的实现类叫做数据库驱动程序,由数据库的厂商或其它的厂商或个人提供。

为了使客户端程序独立于特定的数据库驱动程序,JDBC 规范建议开发者使用基于接口的编程方式,即尽量使应用仅依赖 java.sql 及 javax.sql 中的接口和类。

JDBC 驱动程序是各个数据库厂家根据 JDBC 的规范制作的 JDBC 实现类.

主要涉及到的类有:

  • connection:接口类,mysql 封装了连接数据库的参数,辅助类,提供了 sql 语句执行,创建 statement 对象,提交,回滚等功能。
  • preparedStatement:接口类,保存 sql 执行语句,并提供查询,修改等方法。
  • Driver:驱动类,子类提供了返回 connection 对象方法的实现,以及一些辅助方法
  • DriverManager:驱动管理类,注册 Driver 对象
    下面我们来一步步解析查询数据库的过程:
  1. 驱动类加载

    Class.forName("com.mysql.jdbc.Driver")
    当我们看到这行代码时我们可能会有些疑惑:为什么开始要加载初始化这个驱动?那我们先看看它里面有什么。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //Driver 类
    static {
    try {
    java.sql.DriverManager.registerDriver(new Driver()); //注册驱动
    } catch (SQLException E) {
    throw new RuntimeException("Can't register driver!");
    }
    }
    //DriverManager 类
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    public static synchronized void registerDriver(java.sql.Driver driver,
    DriverAction da)
    throws SQLException {

    /* Register the driver if it has not already been added to our list */
    if(driver != null) {
    registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); //封装注册类并村主导
    } else {
    // This is for compatibility with the original DriverManager
    throw new NullPointerException();
    }

    println("registerDriver: " + driver);

    }

    看到这里我们知道初始化类是为了调用 DriverManager.registerDriver 方法对 Driver 进行注册。在注册方法中将获取到的 Driver 对象封装了一遍存入 registeredDrivers 集合里,这里 registeredDrivers 是 DirverManager 里的一个 list 集合对象,CopyOnWriteArrayList 是一个线程安全 list 集合类。所以 jdbc 可以允许我们在同一项目中加载不同的驱动类去连接多个的数据库。

    Driver 类加载完成之后接下来是
    Connection connection= DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
    这一段是返回 connection 对象的操作,我们看一下 DriverManager 内源码,其中有一段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    static {
    loadInitialDrivers(); /加载初始化其他驱动类
    println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
    String drivers;
    try {
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { //跳过权限验证从系统变量中获取驱动
    public String run() {
    return System.getProperty("jdbc.drivers"); //获取jdbc.drivers变量
    }
    });
    } catch (Exception ex) {
    drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//通过ServiceLoader动态加载驱动类
    Iterator<Driver> driversIterator = loadedDrivers.iterator();

    /* Load these drivers, so that they can be instantiated.
    * It may be the case that the driver class may not be there
    * i.e. there may be a packaged driver with the service class
    * as implementation of java.sql.Driver but the actual class
    * may be missing. In that case a java.util.ServiceConfigurationError
    * will be thrown at runtime by the VM trying to locate
    * and load the service.
    *
    * Adding a try catch block to catch those runtime errors
    * if driver not available in classpath but it's
    * packaged as service and that service is there in classpath.
    */
    try{
    while(driversIterator.hasNext()) {
    driversIterator.next(); //遍历并初始化对象
    }
    } catch(Throwable t) {
    // Do nothing
    }
    return null;
    }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
    return;
    }
    String[] driversList = drivers.split(":"); //从系统变量中获取的完全限定名
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) { //遍历驱动名并初始化
    try {
    println("DriverManager.Initialize: loading " + aDriver);
    Class.forName(aDriver, true,
    ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
    println("DriverManager.Initialize: load failed: " + ex);
    }
    }
    }

    在 DriverMnager 中有一段静态代码块,我们第一次调用时会执行里面的 loadInitialDrivers 方法完成第二次驱动的加载,这里加载驱动的方式有两种。一种是通过获取系统的环境变量 jdbc.drivers 得到驱动类的完全限定名并通过反射进行初始化注册,另一个是通过 serviceLoader(参考 Java 的 SPI)动态获取驱动类对象完成注册,两种方式都在 AccessController.doPrivileged 内执行,是为了跳过虚拟机权限验证

  2. 返回 connection 对象
    再次定位到这行代码

    1
    Connection connection= DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");

    查看 getConnection 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    @CallerSensitive
    public static Connection getConnection(String url,
    String user, String password) throws SQLException {
    java.util.Properties info = new java.util.Properties();

    if (user != null) {
    info.put("user", user);
    }
    if (password != null) {
    info.put("password", password);
    }

    return (getConnection(url, info, Reflection.getCallerClass()));
    }

    private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    /*
    * When callerCl is null, we should check the application's
    * (which is invoking this class indirectly)
    * classloader, so that the JDBC driver class outside rt.jar
    * can be loaded from here.
    */
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; //获取当前线程的类加载器
    synchronized(DriverManager.class) {
    // synchronize loading of the correct classloader.
    if (callerCL == null) {
    callerCL = Thread.currentThread().getContextClassLoader();
    }
    }

    if(url == null) {
    throw new SQLException("The url cannot be null", "08001");
    }

    println("DriverManager.getConnection(\"" + url + "\")");

    // Walk through the loaded registeredDrivers attempting to make a connection.
    // Remember the first exception that gets raised so we can reraise it.
    SQLException reason = null;

    for(DriverInfo aDriver : registeredDrivers) { //遍历注册信息
    // If the caller does not have permission to load the driver then
    // skip it.
    if(isDriverAllowed(aDriver.driver, callerCL)) { //判断该驱动是否是callerCL加载器加载的
    try {
    println(" trying " + aDriver.driver.getClass().getName());
    Connection con = aDriver.driver.connect(url, info); //通过驱动器返回connection对象
    if (con != null) {
    // Success!
    println("getConnection returning " + aDriver.driver.getClass().getName());
    return (con);
    }
    } catch (SQLException ex) {
    if (reason == null) {
    reason = ex;
    }
    }

    } else {
    println(" skipping: " + aDriver.getClass().getName());
    }

    }

    // if we got here nobody could connect.
    if (reason != null) {
    println("getConnection failed: " + reason);
    throw reason;
    }

    println("getConnection: no suitable driver found for "+ url);
    throw new SQLException("No suitable driver found for "+ url, "08001");
    }

    以上通过备注我们知道,这个是一个获取 connection 对象的过程,先是遍历 registerDrivers 集合获取每个驱动器,然后进行验证,成功后返回该驱动器。到这里 jdbc 的源码解析就结束了,因为 sql 包中很多都是接口需要子类进行实现,所以接下来要说的都是 mysql 继承接口中的实现,我也不细说大概点一下。

  3. 获取 PreparedStatement 对象

    1
    PreparedStatement prepareStatement=connection.prepareStatement("select * from student");

    这里我们通过 connection 得到了 preparedstatement,preparedstatement 继承自 statement,里面保存了 sql 语句对象,并提供了查询 sql 的方法。

  4. 获取 ResultSet 对象
    ResultSet resultSet=prepareStatement.executeQuery();

    在这里 statement 对象调用了 executeQuery 方法,里面将会执行发送 sql 以及获取数据的操作,在 mysql 中是通过 Socket 对象进行操作的。方法返回 ResultSet,存储了查询的结果。获取 resultSet 对象后通过 next 方法移动游标定位信息。

    1
    2
    3
    4
    while(resultSet.next()){
    System.out.println(resultSet.getString("id")+":"+resultSet.getString("studname"));
    }

    以上就是 jdbc 源码的讲解。

java.sql 里面有什么?

java.sql 包中包含用于以下方面的 API:

  • 通过 DriverManager 实用程序建立与数据库的连接 bai
  • DriverManager 类:建立与驱动程序的连接
  • SQLPermission 类:代码在 Security Manager(比如 applet)中运行时提供权限,试图通过 DriverManager 设置一个记录流
  • Driver 接口:提供用来注册和连接基于 JDBC 技术(“JDBC 驱动程序”)的驱动程序的 API,通常仅由 DriverManager 类使用
  • DriverPropertyInfo 类:提供 JDBC 驱动程序的属性,不是供一般用户使用的向数据库发送 SQL 语句
  • Statement:用于发送基本 SQL 语句
  • PreparedStatement:用于发送准备好的语句或基本 SQL 语句(派生自 Statement)
  • CallableStatement:用于调用数据库存储过程(派生自 PreparedStatement)
  • Connection 接口:提供创建语句以及管理连接及其属性的方法
  • Savepoint:在事务中提供保存点
  • 获取和更新查询的结果
  • ResultSet 接口
  • SQL 类型到 Java 编程语言中的类和接口的标准映射关系
    • Array 接口:SQL ARRAY 的映射关系
    • Blob 接口:SQL BLOB 的映射关系
    • Clob 接口:SQL CLOB 的映射关系
    • Date 类:SQL DATE 的映射关系
    • …..
  • 元数据
    • DatabaseMetaData 接口:提供有关数据库的信息
    • ResultSetMetaData 接口:提供有关 ResultSet 对象的列的信息
    • ParameterMetaData 接口:提供有关 PreparedStatement 命令的参数的信息
  • 异常
    • SQLException:由大多数方法在访问数据出问题时抛出,以及因为其他原因由其他一些方法抛出
    • SQLWarning:为了指示一个警告而抛出