Hibernate版本 5.1.11
private HqlParser parse(boolean filter) throws TokenStreamException, RecognitionException {
// Parse the query string into an HQL AST.
final HqlParser parser = HqlParser.getInstance( hql );
parser.setFilter( filter );
LOG.debugf( "parse() - HQL: %s", hql );
parser.statement();
final AST hqlAst = parser.getAST();
final NodeTraverser walker = new NodeTraverser( new JavaConstantConverter( factory ) );
walker.traverseDepthFirst( hqlAst );
showHqlAst( hqlAst );
parser.getParseErrorHandler().throwQueryException();
return parser;
}
对于hql的解析的起点在QueryTranslatorImpl的parse()方法中。在这个方法中会调用HqlParse的parse()方法对需要操作的hql生成抽象语法树。getInstance()方法中,只是简单的调用了HqlParse的构造方法,其具体的构造方法如下。
private HqlParser(String hql) {
// The fix for HHH-558...
super( new HqlLexer( new StringReader( hql ) ) );
parseErrorHandler = new ErrorCounter( hql );
// Create nodes that track line and column number.
setASTFactory( new HqlASTFactory() );
}
在这个构造方法中,主要是根据hql生成了相应的StringReader来便于hql的读取,同时生成HqlLexer来完成相应的词法分析,最后将生成的HqlLexer作为词法分析器存放在语法分析器中的成员之一。
HqlLexer继承自HqlBaseLexer,而HqlBaseLexer继承自antlr的类CharScanner,构造方法中主要完成两件事情,首先,将传入的包含了所要分析的hql语句的StringReader保证为CharBuffer,作为成员存放,便于数据的读取。同时根据,将定义在语法g4文件,hql.g4文件中的关键字存放在hashTable中,便于在读取hql中,将相应的char转化成相应的关键字。
语法文件中关于关键字的定义如下:
EXISTS="exists";
FALSE="false";
FETCH="fetch";
FROM="from";
FULL="full";
GROUP="group";
HAVING="having";
IN="in";
INDICES="indices";
而在经过antlr编译后生成的java类HqlSqlTokenTypes中,这些关键字都会相应的一次被赋予对应的Int值。
int EXISTS = 19;
int FALSE = 20;
int FETCH = 21;
int FROM = 22;
int FULL = 23;
int GROUP = 24;
int HAVING = 25;
int IN = 26;
int INDICES = 27;
同时我们可以看到,在相应的HqlBaseLexer构造方法中,这些关键字都会被转化ANTLRHashString作为键,与HqlSqlTokenTypes类中对应的int值作为值,组成键值对,存放在hashTable中。
literals.put(new ANTLRHashString("ascending", this), new Integer(110));
literals.put(new ANTLRHashString("descending", this), new Integer(111));
literals.put(new ANTLRHashString("false", this), new Integer(20));
literals.put(new ANTLRHashString("exists", this), new Integer(19));
literals.put(new ANTLRHashString("asc", this), new Integer(8));
literals.put(new ANTLRHashString("left", this), new Integer(33));
而ANTLRHashString中的构造方法很简单,主要就是存放对应的关键字,以及当前的词法分析器继承了CharScanner的HqlBaseLexer。
在词法分析器HqlBaseLexer的构造方法中,主要完成的是上述个关于关键字与相应的int值对应的处理与查询,在接下来语法分析器Parse具体进行语法分析的时候,会对关键字和符号进行更详细的处理。
在HqlParse调用statment()方法正式进行语法分析来生成抽象语法树的时候,每次都会调用LA(1)来读取下一个抽象为hql语句元素的Token,来作为下一个语法规则进行匹配的对象。如下面的代码。
try { // for error handling
{
switch ( LA(1)) {
case UPDATE:
{
updateStatement();
astFactory.addASTChild(currentAST, returnAST);
break;
}
而在LA()中,最后在HqlBaseLexer的父类TokenBuffer的fill()方法中主要还是调用词法分析器HqlBaseLexer的nextToken()方法,来解析获取当前语句的下一个Token来在Parse中进行语法分析。
private final void fill(int var1) throws TokenStreamException {
this.syncConsume();
while(this.queue.nbrEntries < var1 + this.markerOffset) {
this.queue.append(this.input.nextToken());
}
}
在HqlBaseLexer的nextToken()方法中,通过LA()方法获取当前char流中当前消费位置的char来进行分析,可以通过改变LA()中的参数来获得接下来的某个char元素,在通过match()方法完成相应的关键字匹配后,会通过刷新新的读取位置。完成词法分析的词也就是Token会加入在当前的缓冲流中,并记录该词在流中的起始位置与长度。
在nextToken()的词法分析流程中,首要的是对一些简单符号的分析,例如括号逗号。
switch ( LA(1)) {
case '=':
{
mEQ(true);
theRetToken=_returnToken;
break;
}
case '!': case '^':
{
mNE(true);
theRetToken=_returnToken;
break;
}
case ',':
{
mCOMMA(true);
theRetToken=_returnToken;
break;
}
在类似符号的匹配中,一旦被确认成功,例如等于号,将会通过mEQ()确认相应的Token类型并结束读取数据来分析词法。
public final void mEQ(boolean _createToken) throws RecognitionException, CharStreamException, TokenStreamException {
int _ttype; Token _token=null; int _begin=text.length();
_ttype = EQ;
int _saveIndex;
match('=');
if ( _createToken && _token==null && _ttype!=Token.SKIP ) {
_token = makeToken(_ttype);
_token.setText(new String(text.getBuffer(), _begin, text.length()-_begin));
}
_returnToken = _token;
}
在mEQ()中,首先将代表词Token类型的int值设为括号EQ所对应的值(值跟之前的关键字一样有antlr根据语法文件编译后产生),之后调用match()方法进行再一次检验。
public void match(char var1) throws MismatchedCharException, CharStreamException {
if (this.LA(1) != var1) {
throw new MismatchedCharException(this.LA(1), var1, false, this);
} else {
this.consume();
}
}
match()主要在进行了一次检验,如果确认通过则调用consume()方法刷新下一次的读取位置并将当前char放入缓冲流中便于之后的具体获取。
在完成match()之后,生成括号类型的Token,并记录在缓冲流中的位置与长度,返回交给语法分析器进行语法分析。
在完成对于简单符号的判断之后如果并没有匹配成功,会进行数字的相应处理。
case '.': case '0': case '1': case '2':
case '3': case '4': case '5': case '6':
case '7': case '8': case '9':
{
mNUM_INT(true);
theRetToken=_returnToken;
break;
}
在mNUM_INT()中会对数字进行相应的词法分析。
对数字的分析相应的比较长,以小数点.的分析部分为例子。
switch ( LA(1)) {
case '.':
{
match('.');
if ( inputState.guessing==0 ) {
_ttype = DOT;
}
首先,读取到小数点之后,会通过matcjh(0方法将小数点加入到缓冲流中,并将读取位置刷新到小数点下一位置。
if (((LA(1) >= '0' && LA(1) <= '9'))) {
{
int _cnt353=0;
_loop353:
do {
if (((LA(1) >= '0' && LA(1) <= '9'))) {
matchRange('0','9');
}
else {
if ( _cnt353>=1 ) { break _loop353; } else {throw new NoViableAltForCharException((char)LA(1), getFilename(), getLine(), getColumn());}
}
_cnt353++;
} while (true);
}
之后继续匹配小数点后面的char元素,如果是1到9的数字,继续通过matchRange()完成类似match()的操作来更新下一个读取的位置并加入缓冲流准备返回。
{
if ((LA(1)=='e')) {
mEXPONENT(false);
}
else {
}
}
{
if ((LA(1)=='b'||LA(1)=='d'||LA(1)=='f')) {
mFLOAT_SUFFIX(true);
f1=_returnToken;
if ( inputState.guessing==0 ) {
t=f1;
}
}
else {
}
}
if ( inputState.guessing==0 ) {
if ( t != null && t.getText().toUpperCase().indexOf("BD")>=0) {
_ttype = NUM_BIG_DECIMAL;
}
else if (t != null && t.getText().toUpperCase().indexOf('F')>=0) {
_ttype = NUM_FLOAT;
}
else {
_ttype = NUM_DOUBLE; // assume double
}
}
}
else {
}
}
break;
}
之后如果匹配到e,则会mEXPONENT(),嵌套分析。
protected final void mEXPONENT(boolean _createToken) throws RecognitionException, CharStreamException, TokenStreamException {
int _ttype; Token _token=null; int _begin=text.length();
_ttype = EXPONENT;
int _saveIndex;
{
match('e');
}
{
switch ( LA(1)) {
case '+':
{
match('+');
break;
}
case '-':
{
match('-');
break;
}
case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7':
case '8': case '9':
{
break;
}
default:
{
throw new NoViableAltForCharException((char)LA(1), getFilename(), getLine(), getColumn());
}
}
}
{
int _cnt381=0;
_loop381:
do {
if (((LA(1) >= '0' && LA(1) <= '9'))) {
matchRange('0','9');
}
else {
if ( _cnt381>=1 ) { break _loop381; } else {throw new NoViableAltForCharException((char)LA(1), getFilename(), getLine(), getColumn());}
}
_cnt381++;
} while (true);
}
if ( _createToken && _token==null && _ttype!=Token.SKIP ) {
_token = makeToken(_ttype);
_token.setText(new String(text.getBuffer(), _begin, text.length()-_begin));
}
_returnToken = _token;
}
在进入mEXPONENT()方法之后,会根据match()刷新读取率位置顺势读取e之后的数据。在下面的逻辑中,如果出现了加号减号数字之外的数据,将会报错,而加号减号数字则会同样根据match()完成之前一样的更新缓冲流的操作,在e的解析完成后,由于此次是在数字中的解析,e只是数字的一部分,不会生成相应的Token所有只是简单结束并继续当前数字分析。
e之后是bdf三个字母对于数字后缀的解析,在mFLOAT_SUFFIX()方法进行处理,由于出现这种字母代表数字已经进入结尾,所以将会生成Token记录该数字的类型,并结束对于这个数字的词法分析。
if ( t != null && t.getText().toUpperCase().indexOf("BD")>=0) {
_ttype = NUM_BIG_DECIMAL;
}
else if (t != null && t.getText().toUpperCase().indexOf('F')>=0) {
_ttype = NUM_FLOAT;
}
else {
_ttype = NUM_DOUBLE; // assume double
}
最后会根据数字的后缀来判定数字在HQL中的类型。
if ( _createToken && _token==null && _ttype!=Token.SKIP ) {
_token = makeToken(_ttype);
_token.setText(new String(text.getBuffer(), _begin, text.length()-_begin));
}
_returnToken = _token;
生成的数字与之前一样生成对应类型的Token,并记录在缓冲流中的开始位置与长度。
在完成数字分析,是对大于小于的词法分析,相对简单,之后是对关键字或者hql中字符常量的分析,在mIDENT()方法中完成。
public final void mIDENT(boolean _createToken) throws RecognitionException, CharStreamException, TokenStreamException {
int _ttype; Token _token=null; int _begin=text.length();
_ttype = IDENT;
int _saveIndex;
mID_START_LETTER(false);
{
_loop339:
do {
if ((_tokenSet_1.member(LA(1)))) {
mID_LETTER(false);
}
else {
break _loop339;
}
} while (true);
}
if ( inputState.guessing==0 ) {
// Setting this flag allows the grammar to use keywords as identifiers, if necessary.
setPossibleID(true);
}
_ttype = testLiteralsTable(_ttype);
if ( _createToken && _token==null && _ttype!=Token.SKIP ) {
_token = makeToken(_ttype);
_token.setText(new String(text.getBuffer(), _begin, text.length()-_begin));
}
_returnToken = _token;
}
此处的重点主要在于关键字和字符常量的判定,在按照之前的步骤完成词语的读取完毕之后,调用testLiteralsTable()方法。
public int testLiteralsTable(int var1) {
this.hashString.setBuffer(this.text.getBuffer(), this.text.length());
Integer var2 = (Integer)this.literals.get(this.hashString);
if (var2 != null) {
var1 = var2;
}
return var1;
}
在这个方法中,从缓冲流获取读取的词在构造方法中放入的关键字的HashTable寻找,如果找到则返回相应的int值,也就是读取到了关键字,并返回关键字对应的int值。如果没有,则作为常量类型返回。
以上主要就是关于antlr对于hql的词法分析。