簡單語法解析器實現參考

  有時候,咱們爲了屏蔽一些底層的差別,咱們會要求上游系統按照某種約定進行傳參。而在咱們本身的系統層則會按照具體的底層協議進行適配,這是通用的作法。但當咱們要求上游系統傳入的參數很是複雜時,也許咱們會有一套本身的語法定義,用以減輕全部參數的不停變化。好比sql協議,就是一個一級棒的語法,一樣是調用底層功能,但它能夠很方便地讓用戶傳入任意的參數。java

  若是咱們本身可以實現一套相似的東西,想來應該蠻有意思的。算法

  不過,咱們徹底沒有必要要實現一整套完整的東西,咱們只是要體驗下這個語法解析的過程,那就好辦了。本文就來給個簡單的解析示例,供看官們參考。sql

 

1. 實現目標描述

  目標:數據庫

    基於咱們自定義的一套語法,咱們要實現一套相似於sql解析的工具,它能夠幫助咱們檢查語法錯誤、應對底層不一樣的查詢引擎,好比多是 ES, HIVE, SPARK, PRESTO...  即咱們可能將用戶傳入的語法轉換爲任意種目標語言的語法,這是咱們的核心任務。apache

  前提:數組

    爲簡單化起見,咱們並不想實現一整套的東西,咱們僅處理where條件後面的東西。
  定義:
    $1234: 定義爲字段信息, 咱們能夠經過該字段查找出一些更多的信息;
    and/or/like...: 大部分時候咱們都遵循sql語法, 含義一致;
    #{xxx}: 系統關鍵字定義格式, xxx 爲具體的關鍵字;
    arr['f1']: 爲數組格式的字段;mvc

  示例:app

    $15573 = 123 and (my_udf($123568, $82949) = 1) or $39741 = #{day+1} and $35289 like '%ccc'
    將會被翻譯成ES:(更多信息的字段替換請忽略)
    $15573 = 123 and ( $123568 = 1 ) or $39741 = '2020-10-07' and $35289 like '%ccc'
ide

  實際上整個看下來,和一道普通的算法題差不太多呢。但實際想要完整實現這麼個小東西,也是要費很多精力的。函數

 

2. 總體實現思路

  咱們要作一個解析器,或者說翻譯器,首先第一步,天然是要從根本上理解原語義,而後再根據目標語言的表達方式,轉換過去就能夠了。

  若是你們有看過一些編譯原理方面的書,就應該知道,整個編譯流程大概分爲: 詞法分析;語法分析;語義分析;中間代碼生成;代碼優化;目標代碼; 這麼幾個過程,而每一個過程每每又是很是複雜的,而最複雜的每每又是其上下文關係。不過,咱們不想搞得那麼複雜(也搞不了)。

  雖然咱們不像作一個編譯器同樣複雜,但咱們仍然能夠參考其流程,能夠爲咱們提供比較好的思路。

  咱們就主要作3步就能夠了:1. 分詞;2. 語義分析; 3. 目標代碼生成;並且爲了進一步簡化工做,咱們省去了複雜的上下文依賴分析,咱們假設全部的語義均可以從第一個關鍵詞中得到,好比遇到一個函數,我就知道接下來會有幾個參數出現。並且咱們不處理嵌套關係。

  因此,咱們的工做就變得簡單起來。

 

3. 具體代碼實現

  咱們作這個解析器的目的,是爲了讓調用者方便,它僅僅做爲一個工具類存在,因此,咱們須要將入口作得很是簡單。

  這裏主要爲分爲兩個入口:1. 傳入原始語法,返回解析出的語法樹; 2. 調用語法樹的translateTo 方法,將原始語法轉換爲目標語法;

具體以下:

    
import com.my.mvc.app.common.helper.parser.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.*;

/**
 * 功能描述: 簡單語法解析器實現示例
 *
 */
@Slf4j
public class SimpleSyntaxParser {

    /**
     * 嚴格模式解析語法
     *
     * @see #parse(String, boolean)
     */
    public static ParsedClauseAst parse(String rawClause) {
        return parse(rawClause, true);
    }

    /**
     * 解析傳入詞爲db可識別的語法
     *
     * @param rawClause 原始語法, 如:
     *                  $15573 = 123 and (week_diff($123568, $82949) = 1) or $39741 like '%abc%' (week_diff($35289)) = -1
     * @param strictMode 是不是嚴格模式, true:是, false:否
     * @return 解析後的結構
     */
    public static ParsedClauseAst parse(String rawClause, boolean strictMode) {
        log.info("開始解析: " + rawClause);
        List<TokenDescriptor> tokens = tokenize(rawClause, strictMode);
        Map<String, Object> idList = enhanceTokenType(tokens);
        return buildAst(tokens, idList);
    }

    /**
     * 構建抽象語法樹對象
     *
     * @param tokens 分詞解析出的tokens
     * @param idList id信息(解析數據源參照)
     * @return 構建好的語法樹
     */
    private static ParsedClauseAst buildAst(List<TokenDescriptor> tokens,
                                            Map<String, Object> idList) {
        List<SyntaxStatement> treesFlat = new ArrayList<>(tokens.size());
        Iterator<TokenDescriptor> tokenItr = tokens.iterator();
        while (tokenItr.hasNext()) {
            TokenDescriptor token = tokenItr.next();
            String word = token.getRawWord();
            TokenTypeEnum tokenType = token.getTokenType();
            SyntaxStatement branch;
            switch (tokenType) {
                case FUNCTION_SYS_CUSTOM:
                    String funcName = word.substring(0, word.indexOf('(')).trim();
                    SyntaxStatementHandlerFactory handlerFactory
                            = SyntaxSymbolTable.getUdfHandlerFactory(funcName);
                    branch = handlerFactory.newHandler(token, tokenItr, tokenType);
                    treesFlat.add(branch);
                    break;
                case KEYWORD_SYS_CUSTOM:
                    branch = SyntaxSymbolTable.getSysKeywordHandlerFactory()
                                .newHandler(token, tokenItr, tokenType);
                    treesFlat.add(branch);
                    break;
                case KEYWORD_SQL:
                    branch = SyntaxSymbolTable.getSqlKeywordHandlerFactory()
                                .newHandler(token, tokenItr, tokenType);
                    treesFlat.add(branch);
                    break;
                case WORD_NORMAL:
                case WORD_NUMBER:
                case WORD_STRING:
                case CLAUSE_SEPARATOR:
                case SIMPLE_MATH_OPERATOR:
                case WORD_ARRAY:
                case COMPARE_OPERATOR:
                case FUNCTION_NORMAL:
                case ID:
                case FUNCTION_SQL:
                default:
                    // 未解析的狀況,直接使用原始值解析器處理
                    branch = SyntaxSymbolTable.getCommonHandlerFactory()
                                .newHandler(token, tokenItr, tokenType);
                    treesFlat.add(branch);
                    break;
            }
        }
        return new ParsedClauseAst(idList, treesFlat);
    }

    /**
     * 語義加強處理
     *
     *      增強token類型描述,並返回 id 信息
     */
    private static Map<String, Object> enhanceTokenType(List<TokenDescriptor> tokens) {
        Map<String, Object> idList = new HashMap<>();
        for (TokenDescriptor token : tokens) {
            String word = token.getRawWord();
            TokenTypeEnum newTokenType = token.getTokenType();
            switch (token.getTokenType()) {
                case WORD_NORMAL:
                    if(word.startsWith("$")) {
                        newTokenType = TokenTypeEnum.ID;
                        idList.put(word, word.substring(1));
                    }
                    else if(StringUtils.isNumeric(word)) {
                        newTokenType = TokenTypeEnum.WORD_NUMBER;
                    }
                    else {
                        newTokenType = SyntaxSymbolTable.keywordTypeOf(word);
                    }
                    token.changeTokenType(newTokenType);
                    break;
                case WORD_STRING:
                    // 被引號包圍的關鍵字,如 '%#{monthpart}%'
                    String innerSysCustomKeyword = readSplitWord(
                            word.toCharArray(), 1, "#{", "}");
                    if(innerSysCustomKeyword.length() > 3) {
                        newTokenType = TokenTypeEnum.KEYWORD_SYS_CUSTOM;
                    }
                    token.changeTokenType(newTokenType);
                    break;
                case FUNCTION_NORMAL:
                    newTokenType = SyntaxSymbolTable.functionTypeOf(word);
                    token.changeTokenType(newTokenType);
                    break;
            }
        }
        return idList;
    }

    /**
     * 查詢語句分詞操做
     *
     *      拆分爲單個細粒度的詞如:
     *          單詞
     *          分隔符
     *          運算符
     *          數組
     *          函數
     *
     * @param rawClause 原始查詢語句
     * @param strictMode 是不是嚴格模式, true:是, false:否
     * @return token化的單詞
     */
    private static List<TokenDescriptor> tokenize(String rawClause, boolean strictMode) {
        char[] clauseItr = rawClause.toCharArray();
        List<TokenDescriptor> parsedTokenList = new ArrayList<>();
        Stack<ColumnNumDescriptor> specialSeparatorStack = new Stack<>();
        int clauseLength = clauseItr.length;
        StringBuilder field;
        String fieldGot;
        char nextChar;

        outer:
        for (int i = 0; i < clauseLength; ) {
            char currentChar = clauseItr[i];
            switch (currentChar) {
                case '\'':
                case '\"':
                    fieldGot = readSplitWord(clauseItr, i,
                            currentChar, currentChar);
                    i += fieldGot.length();
                    parsedTokenList.add(
                            new TokenDescriptor(fieldGot, TokenTypeEnum.WORD_STRING));
                    continue outer;
                case '[':
                case ']':
                case '(':
                case ')':
                case '{':
                case '}':
                    if(specialSeparatorStack.empty()) {
                        specialSeparatorStack.push(
                                ColumnNumDescriptor.newData(i, currentChar));
                        parsedTokenList.add(
                                new TokenDescriptor(currentChar,
                                        TokenTypeEnum.CLAUSE_SEPARATOR));
                        break;
                    }
                    parsedTokenList.add(
                            new TokenDescriptor(currentChar,
                                    TokenTypeEnum.CLAUSE_SEPARATOR));
                    char topSpecial = specialSeparatorStack.peek().getKeyword().charAt(0);
                    if(topSpecial == '(' && currentChar == ')'
                            || topSpecial == '[' && currentChar == ']'
                            || topSpecial == '{' && currentChar == '}') {
                        specialSeparatorStack.pop();
                        break;
                    }
                    specialSeparatorStack.push(
                            ColumnNumDescriptor.newData(i, currentChar));
                    break;
                case ' ':
                    // 空格忽略
                    break;
                case '@':
                    nextChar = clauseItr[i + 1];
                    // @{} 擴展id, 暫不解析, 原樣返回
                    if(nextChar == '{') {
                        fieldGot = readSplitWord(clauseItr, i,
                                "@{", "}@");
                        i += fieldGot.length();
                        parsedTokenList.add(
                                new TokenDescriptor(fieldGot,
                                        TokenTypeEnum.ID));
                        continue outer;
                    }
                    break;
                case '#':
                    nextChar = clauseItr[i + 1];
                    // #{} 系統關鍵字標識
                    if(nextChar == '{') {
                        fieldGot = readSplitWord(clauseItr, i,
                                "#{", "}");
                        i += fieldGot.length();
                        parsedTokenList.add(
                                new TokenDescriptor(fieldGot,
                                        TokenTypeEnum.KEYWORD_SYS_CUSTOM));
                        continue outer;
                    }
                    break;
                case '+':
                case '-':
                case '*':
                case '/':
                    nextChar = clauseItr[i + 1];
                    if(currentChar == '-'
                            && nextChar >= '0' && nextChar <= '9') {
                        StringBuilder numberBuff = new StringBuilder(currentChar + "" + nextChar);
                        ++i;
                        while ((i + 1) < clauseLength){
                            nextChar = clauseItr[i + 1];
                            if(nextChar >= '0' && nextChar <= '9'
                                    || nextChar == '.') {
                                ++i;
                                numberBuff.append(nextChar);
                                continue;
                            }
                            break;
                        }
                        parsedTokenList.add(
                                new TokenDescriptor(numberBuff.toString(),
                                        TokenTypeEnum.WORD_NUMBER));
                        break;
                    }
                    parsedTokenList.add(
                            new TokenDescriptor(currentChar,
                                    TokenTypeEnum.SIMPLE_MATH_OPERATOR));
                    break;
                case '=':
                case '>':
                case '<':
                case '!':
                    // >=, <=, !=, <>
                    nextChar = clauseItr[i + 1];
                    if(nextChar == '='
                            || currentChar == '<' && nextChar == '>') {
                        ++i;
                        parsedTokenList.add(
                                new TokenDescriptor(currentChar + "" + nextChar,
                                        TokenTypeEnum.COMPARE_OPERATOR));
                        break;
                    }
                    parsedTokenList.add(
                            new TokenDescriptor(currentChar,
                                    TokenTypeEnum.COMPARE_OPERATOR));
                    break;
                default:
                    field = new StringBuilder();
                    TokenTypeEnum tokenType = TokenTypeEnum.WORD_NORMAL;
                    do {
                        currentChar = clauseItr[i];
                        field.append(currentChar);
                        if(i + 1 < clauseLength) {
                            // 去除函數前置名後置空格
                            if(SyntaxSymbolTable.isUdfPrefix(field.toString())) {
                                do {
                                    if(clauseItr[i + 1] != ' ') {
                                        break;
                                    }
                                    ++i;
                                } while (i + 1 < clauseLength);
                            }
                            nextChar = clauseItr[i + 1];
                            if(nextChar == '(') {
                                fieldGot = readSplitWord(clauseItr, i + 1,
                                        nextChar, ')');
                                field.append(fieldGot);
                                tokenType = TokenTypeEnum.FUNCTION_NORMAL;
                                i += fieldGot.length();
                                break;
                            }
                            if(nextChar == '[') {
                                fieldGot = readSplitWord(clauseItr, i + 1,
                                        nextChar, ']');
                                field.append(fieldGot);
                                tokenType = TokenTypeEnum.WORD_ARRAY;
                                i += fieldGot.length();
                                break;
                            }
                            if(isSpecialChar(nextChar)) {
                                // 嚴格模式下,要求 -+ 符號先後必須帶空格, 即會將全部字母后緊連的 -+ 視爲字符鏈接號
                                // 非嚴格模式下, 即只要是分隔符即中止字符解析(非標準分隔)
                                if(!strictMode
                                        || nextChar != '-' && nextChar != '+') {
                                    break;
                                }
                            }
                            ++i;
                        }
                    } while (i + 1 < clauseLength);
                    parsedTokenList.add(
                            new TokenDescriptor(field.toString(), tokenType));
                    break;
            }
            // 正常單字解析迭代
            i++;
        }
        if(!specialSeparatorStack.empty()) {
            ColumnNumDescriptor lineNumTableTop = specialSeparatorStack.peek();
            throw new RuntimeException("檢測到未閉合的符號, near '"
                        + lineNumTableTop.getKeyword()+ "' at column "
                        + lineNumTableTop.getColumnNum());
        }
        return parsedTokenList;
    }

    /**
     * 從源數組中讀取某類詞數據
     *
     * @param src 數據源
     * @param offset 要搜索的起始位置 offset
     * @param openChar word 的開始字符,用於避免循環嵌套 如: '('
     * @param closeChar word 的閉合字符 如: ')'
     * @return 解析出的字符
     * @throws RuntimeException 解析不到正確的單詞時拋出
     */
    private static String readSplitWord(char[] src, int offset,
                                        char openChar, char closeChar)
            throws RuntimeException {
        StringBuilder builder = new StringBuilder();
        for (int i = offset; i < src.length; i++) {
            if(openChar == src[i]) {
                int aroundOpenCharNum = -1;
                do {
                    builder.append(src[i]);
                    // 注意 openChar 能夠 等於 closeChar
                    if(src[i] == openChar) {
                        aroundOpenCharNum++;
                    }
                    if(src[i] == closeChar) {
                        aroundOpenCharNum--;
                    }
                } while (++i < src.length
                        && (aroundOpenCharNum > 0 || src[i] != closeChar));
                if(aroundOpenCharNum > 0
                        || (openChar == closeChar && aroundOpenCharNum != -1)) {
                    throw new RuntimeException("syntax error, un closed clause near '"
                            + builder.toString() + "' at column " + --i);
                }
                builder.append(closeChar);
                return builder.toString();
            }
        }
        // 未找到匹配
        return "";
    }

    /**
     * 重載另外一版,適用特殊場景 (不支持嵌套)
     *
     * @see #readSplitWord(char[], int, char, char)
     */
    private static String readSplitWord(char[] src, int offset,
                                        String openChar, String closeChar)
            throws RuntimeException {
        StringBuilder builder = new StringBuilder();
        for (int i = offset; i < src.length; i++) {
            if(openChar.charAt(0) == src[i]) {
                int j = 0;
                while (++j < openChar.length() && ++i < src.length) {
                    if(openChar.charAt(j) != src[i]) {
                        break;
                    }
                }
                // 未匹配開頭
                if(j < openChar.length()) {
                    continue;
                }
                builder.append(openChar);
                while (++i < src.length){
                    int k = 0;
                    if(src[i] == closeChar.charAt(0)) {
                        while (++k < closeChar.length() && ++i < src.length) {
                            if(closeChar.charAt(k) != src[i]) {
                                break;
                            }
                        }
                        if(k < closeChar.length()) {
                            throw new RuntimeException("un closed syntax, near '"
                                    + new String(src, i - k, k)
                                    + ", at column " + (i - k));
                        }
                        builder.append(closeChar);
                        break;
                    }
                    builder.append(src[i]);
                }
                return builder.toString();
            }
        }
        // 未找到匹配
        return " ";
    }

    /**
     * 檢測字符是否特殊運算符
     *
     * @param value 給定檢測字符
     * @return true:是特殊字符, false:普通
     */
    private static boolean isSpecialChar(char value) {
        return SyntaxSymbolTable.OPERATOR_ALL.indexOf(value) != -1;
    }

}

  入口便是 parse() 方法。其中,着重須要說明的是:咱們必需要完整解釋出全部語義,因此,咱們須要爲每一個token作類型定義,且每一個具體語法須要有相應的處理器進行處理。這些東西,在解析完成時就是固定的了。但具體須要翻譯成什麼語言,須要由用戶進行定義,以便靈活使用。

  接下來咱們來看看如何進行翻譯:

import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.Map;

/**
 * 功能描述: 解析出的各小塊語句
 *
 */
@Slf4j
public class ParsedClauseAst {

    /**
     * id 信息容器
     */
    private Map<String, Object> idMapping;

    /**
     * 語法樹 列表
     */
    private List<SyntaxStatement> ast;

    public ParsedClauseAst(Map<String, Object> idMapping,
                           List<SyntaxStatement> ast) {
        this.idMapping = idMapping;
        this.ast = ast;
    }

    public Map<String, Object> getidMapping() {
        return idMapping;
    }

    /**
     * 轉換語言表達式
     *
     * @param sqlType sql類型
     * @see TargetDialectTypeEnum
     * @return 翻譯後的sql語句
     */
    public String translateTo(TargetDialectTypeEnum sqlType) {
        StringBuilder builder = new StringBuilder();
        for (SyntaxStatement tree : ast) {
            builder.append(tree.translateTo(sqlType));
        }
        String targetCode = builder.toString().trim();
        log.info("翻譯成目標語言:{}, targetCode: {}", sqlType, targetCode);
        return targetCode;
    }

    @Override
    public String toString() {
        return "ParsedClauseAst{" +
                "idMapping=" + idMapping +
                ", ast=" + ast +
                '}';
    }
}

  這裏的翻譯過程,實際上就是一個委託的過程,由於全部的語義都已被封裝到具體的處理器中,因此咱們只需處理好各細節就能夠了。最後將全部小語句拼接起來,就獲得咱們最終要的目標語言了。因此,具體翻譯的重點工做,須要各自處理,這是很合理的事。

  大致的思路和實現就是如上,着實也簡單。但可能你還跑不起來以上 demo, 由於還有很是多的細節。

 

4. token類型定義

  咱們須要爲每個token有一個準確的描述,以便在後續的處理中,可以準確處理。

/**
 * 功能描述: 拆分的token 描述
 *
 */
public class TokenDescriptor {

    /**
     * 原始字符串
     */
    private String rawWord;

    /**
     * token類型
     *
     *      用於肯定如何使用該token
     *      或者該token是如何被分割出的
     */
    private TokenTypeEnum tokenType;

    public TokenDescriptor(String rawWord, TokenTypeEnum tokenType) {
        this.rawWord = rawWord;
        this.tokenType = tokenType;
    }

    public TokenDescriptor(char rawWord, TokenTypeEnum tokenType) {
        this.rawWord = rawWord + "";
        this.tokenType = tokenType;
    }

    public void changeTokenType(TokenTypeEnum tokenType) {
        this.tokenType = tokenType;
    }

    public String getRawWord() {
        return rawWord;
    }

    public TokenTypeEnum getTokenType() {
        return tokenType;
    }

    @Override
    public String toString() {
        return "T{" +
                "rawWord='" + rawWord + '\'' +
                ", tokenType=" + tokenType +
                '}';
    }
}

// ------------- TokenTypeEnum -----------------
/**
 * 功能描述: 單個不可分割的token 類型定義
 *
 */
public enum TokenTypeEnum {

    LABEL_ID("基礎id如$123"),

    FUNCTION_NORMAL("是函數但類型未知(未解析)"),

    FUNCTION_SYS_CUSTOM("系統自定義函數如week_diff(a)"),

    FUNCTION_SQL("sql中自帶函數如date_diff(a)"),

    KEYWORD_SYS_CUSTOM("系統自定義關鍵字如datepart"),

    KEYWORD_SQL("sql中自帶的關鍵字如and"),

    CLAUSE_SEPARATOR("語句分隔符,如'\"(){}[]"),

    SIMPLE_MATH_OPERATOR("簡單數學運算符如+-*/"),

    COMPARE_OPERATOR("比較運算符如=><!=>=<="),

    WORD_ARRAY("數組類型字段如 arr['key1']"),

    WORD_STRING("字符型具體值如 '%abc'"),

    WORD_NUMBER("數字型具體值如 123.4"),

    WORD_NORMAL("普通字段能夠是數據庫字段也能夠是用戶定義的字符"),

    ;

    private TokenTypeEnum(String remark) {
        // ignore...
    }
}

  如上,基本能夠描述各詞的類型了,若是不夠,咱們能夠視狀況新增便可。從這裏,咱們能夠準確地看出一些分詞的規則。

 

5. 符號表的定義

  很明顯,咱們須要一個統籌全部可被處理的詞組的地方,這就是符號表,咱們能夠經過符號表,準確的查到哪些是系統關鍵詞,哪些是udf,哪些是被支持的方法等等。這是符號表的職責。並且,符號表也能夠支持註冊,從而使其可擴展。具體以下:

import com.my.mvc.app.common.helper.parser.keyword.SysCustomKeywordAstHandler;
import com.my.mvc.app.common.helper.parser.udf.SimpleUdfAstHandler;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 功能描述: 語法符號表(提供查詢入口)
 */
public class SyntaxSymbolTable {

    /**
     * 全部操做符
     */
    public static final String OPERATOR_ALL = "'\"[ ](){}=+-*/><!";

    /**
     * 全部處理器
     */
    private static final Map<String, SyntaxStatementHandlerFactory> handlers
            = new ConcurrentHashMap<>();

    private static final String SYS_CUSTOM_KEYWORD_REF_NAME = "__sys_keyword_handler";

    private static final String SQL_KEYWORD_REF_NAME = "__sql_keyword_handler";

    private static final String COMMON_HANDLER_REF_NAME = "__common_handler";

    static {
        // 註冊udf, 也能夠放到外部調用
        registerUdf(
                (masterToken, candidates, handlerType)
                        -> new SimpleUdfAstHandler(masterToken, candidates,
                TokenTypeEnum.FUNCTION_SYS_CUSTOM),
                "week_diff", "fact.week_diff", "default.week_diff");

        // 註冊系統自定義關鍵字處理器
        handlers.putIfAbsent(SYS_CUSTOM_KEYWORD_REF_NAME, SysCustomKeywordAstHandler::new);

        // 註冊兜底處理器
        handlers.putIfAbsent(COMMON_HANDLER_REF_NAME, CommonConditionAstBranch::new);
    }

    /**
     * 判斷給定詞彙的 keyword 類型
     *
     * @param keyword 指定判斷詞
     * @return 系統自定義關鍵字、sql關鍵字、普通字符
     */
    public static TokenTypeEnum keywordTypeOf(String keyword) {
        if("datepart".equals(keyword)) {
            return TokenTypeEnum.KEYWORD_SYS_CUSTOM;
        }
        if("and".equals(keyword)
                || "or".equals(keyword)
                || "in".equals(keyword)) {
            return TokenTypeEnum.KEYWORD_SQL;
        }
        return TokenTypeEnum.WORD_NORMAL;
    }

    /**
     * 註冊一個 udf 處理器實例
     *
     * @param handlerFactory 處理器工廠類
     *              tokens 必要參數列表,說到底自定義
     * @param callNameAliases 函數調用別名, 如 wee_diff, fact.week_diff...
     */
    public static void registerUdf(SyntaxStatementHandlerFactory handlerFactory,
                                   String... callNameAliases) {
        for (String alias : callNameAliases) {
            handlers.put(alias, handlerFactory);
        }
    }

    /**
     * 獲取udf處理器的工廠類 (可用於斷定系統是否支持)
     *
     * @param udfFunctionName 函數名稱
     * @return 對應的工廠類
     */
    public static SyntaxStatementHandlerFactory getUdfHandlerFactory(String udfFunctionName) {
        SyntaxStatementHandlerFactory factory= handlers.get(udfFunctionName);
        if(factory == null) {
            throw new RuntimeException("不支持的函數操做: " + udfFunctionName);
        }
        return factory;
    }

    /**
     * 獲取系統自定義關鍵字處理器的工廠類  應固定格式爲 #{xxx+1}
     *
     * @return 對應的工廠類
     */
    public static SyntaxStatementHandlerFactory getSysKeywordHandlerFactory() {
        return handlers.get(SYS_CUSTOM_KEYWORD_REF_NAME);
    }

    /**
     * 獲取sql關鍵字處理器的工廠類  遵照 sql 協議
     *
     * @return 對應的工廠類
     */
    public static SyntaxStatementHandlerFactory getSqlKeywordHandlerFactory() {
        return handlers.get(COMMON_HANDLER_REF_NAME);
    }

    /**
     * 獲取通用處理器的工廠類(兜底)
     *
     * @return 對應的工廠類
     */
    public static SyntaxStatementHandlerFactory getCommonHandlerFactory() {
        return handlers.get(COMMON_HANDLER_REF_NAME);
    }


    /**
     * 檢測名稱是不是udf 函數前綴
     *
     * @param udfFunctionName 函數名稱
     * @return true:是, false:其餘關鍵詞
     */
    public static boolean isUdfPrefix(String udfFunctionName) {
        return handlers.get(udfFunctionName) != null;
    }

    /**
     * 判斷給定詞彙的 keyword 類型
     *
     * @param functionFullDesc 函數總體使用方式
     * @return 系統自定義函數,系統函數、未知
     */
    public static TokenTypeEnum functionTypeOf(String functionFullDesc) {
        String funcName = functionFullDesc.substring(0, functionFullDesc.indexOf('('));
        funcName = funcName.trim();
        if("my_udf".equals(funcName)) {
            return TokenTypeEnum.FUNCTION_SYS_CUSTOM;
        }
        return TokenTypeEnum.FUNCTION_NORMAL;
    }

}

  實際上,整個解析器的完善過程,大部分時候就是符號表的一個完善過程。支持的符號越多了,則功能就越完善了。咱們經過一個個的工廠類,實現了具體解析類的細節,屏蔽到內部的變化,從而使變化對上層的無感知。

  如下爲處理器的定義,及工廠類定義:

import java.util.Iterator;

/**
 * 功能描述: 組合標籤語句處理器 工廠類
 *
 *      生產提供各處理器實例
 *
 */
public interface SyntaxStatementHandlerFactory {

    /*
     * 獲取本語句對應的操做數量
     *
     *      其中, 函數調用會被解析爲單token, 如 my_udf($123) = -1
     *          my_udf($123) 爲函數調用, 算一個token
     *          '=' 爲運算符,算第二個token
     *          '-1' 爲右值, 算第三個token
     *      因此此例應返回 3
     *
     * 此實現由具體的 StatementHandler 處理
     *      從 candidates 中獲取便可
     *
     */
    /**
     * 生成一個新的語句處理器實例
     *
     * @param masterToken 主控token, 如關鍵詞,函數調用...
     * @param candidates 候選詞組(後續詞組), 此實現基於本解析器無全局說到底關聯性
     * @param handlerType 處理器類型,如函數、關鍵詞、sql...
     * @return 對應的處理器實例
     */
    SyntaxStatement newHandler(TokenDescriptor masterToken,
                               Iterator<TokenDescriptor> candidates,
                               TokenTypeEnum handlerType);
}    
    

// ----------- SyntaxStatement ------------------        
/**
 * 功能描述: 單個小詞組處理器
 *
 */
public interface SyntaxStatement {

    /**
     * 轉換成目標語言表示
     *
     * @param targetSqlType 目標語言類型 es|hive|presto|spark
     * @return 翻譯後的語言表示
     */
    String translateTo(TargetDialectTypeEnum targetSqlType);

}

  有了這符號表和處理器的接口定義,後續的工做明顯方便不少。

  最後,還有一個行號指示器,須要定義下。它能夠幫助咱們給出準確的錯誤信息提示,從而減小排錯時間。

    
/**
 * 功能描述: 行列號指示器
 */
public class ColumnNumDescriptor {

    /**
     * 列號
     */
    private int columnNum;

    /**
     * 關鍵詞
     */
    private String keyword;

    public ColumnNumDescriptor(int columnNumFromZero, String keyword) {
        this.columnNum = columnNumFromZero + 1;
        this.keyword = keyword;
    }

    public static ColumnNumDescriptor newData(int columnNum, String data) {
        return new ColumnNumDescriptor(columnNum, data);
    }
    public static ColumnNumDescriptor newData(int columnNum, char dataChar) {
        return new ColumnNumDescriptor(columnNum, dataChar + "");
    }

    public int getColumnNum() {
        return columnNum;
    }

    public String getKeyword() {
        return keyword;
    }

    @Override
    public String toString() {
        return "Col{" +
                "columnNum=" + columnNum +
                ", keyword='" + keyword + '\'' +
                '}';
    }
}

  

6. 目標語言定義

  系統可支持的目標語言是有限的,應當將其定義爲枚舉類型,以便用戶規範使用。

/**
 * 功能描述: 組合標籤可被翻譯成的 方言枚舉
 *
 */
public enum TargetDialectTypeEnum {
    ES,
    HIVE,
    PRESTO,
    SPARK,

    /**
     * 原始語句
     */
    RAW,

    ;
}

  若是有一天,你新增了一個語言的實現,那你就能夠將類型加上,這樣用戶也就能夠調用了。

 

7. 詞義處理器實現示例

  解析器的幾大核心之一就是詞義處理器,前面不少的工做都是準備性質的,好比分詞,定義等。前面也看到,咱們將詞義處理器統必定義了一個接口: SyntaxStatement . 即全部詞義處理,都只需實現該接口便可。但該詞義至少得獲取到相應的參數,因此經過一個通用的工廠類生成該處理器,也即須要在構造器中處理好上下文關係。

  首先,咱們須要有一個兜底的處理器,以便在未知的狀況下,能夠保證原語義正確,而非直接出現異常,除非確認全部語義已實現,不然該兜底處理器都是有存在的必要的。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 功能描述: 通用抽象語法樹處理器(分支)
 *
 */
public class CommonConditionAstBranch implements SyntaxStatement {

    /**
     * 擴展詞組列表(如 = -1, > xxx ...)
     *
     *      至關於詞組上下文
     */
    protected final List<TokenDescriptor> extendTokens = new ArrayList<>();

    /**
     * 類型: 函數, 關鍵詞, 分隔符...
     */
    protected TokenTypeEnum tokenType;

    /**
     * 主控詞(如   and, my_udf($123))
     *
     *      可用於肯定該語義大方向
     */
    protected TokenDescriptor masterToken;


    public CommonConditionAstBranch(TokenDescriptor masterToken,
                                    Iterator<TokenDescriptor> candidates,
                                    TokenTypeEnum tokenType) {
        this.masterToken = masterToken;
        this.tokenType = tokenType;
        for (int i = 0; i < getFixedExtTokenNum(); i++) {
            if(!candidates.hasNext()) {
                throw new RuntimeException("用法不正確: ["
                        + masterToken.getRawWord() + "] 缺乏變量");
            }
            addExtendToken(candidates.next());
        }
    }

    /**
     * 添加附加詞組,根據各解析器須要添加
     */
    protected void addExtendToken(TokenDescriptor token) {
        extendTokens.add(token);
    }

    @Override
    public String translateTo(TargetDialectTypeEnum targetSqlType) {
        String separator = " ";
        StringBuilder sb = new StringBuilder(masterToken.getRawWord()).append(separator);
        extendTokens.forEach(r -> sb.append(r.getRawWord()).append(separator));
        return sb.toString();
    }

    /**
     * 解析方法固定參數數量,由父類統一解析
     */
    protected int getFixedExtTokenNum() {
        return 0;
    }

    @Override
    public String toString() {
        return "CTree{" +
                "extendTokens=" + extendTokens +
                ", tokenType=" + tokenType +
                ", masterToken=" + masterToken +
                '}';
    }
}

  該處理器被註冊到符號表中,以 __common_handler 查找。

  接下來,咱們再另外一個處理器的實現: udf。 udf 即用戶自定義函數,這應該是標準sql協議中不存在的關鍵詞,爲業務須要而自行實現的函數,它在有的語言裏,能夠表現爲註冊後的函數,而在有語言裏,咱們只能轉換爲其餘更直接的語法,方可運行。該處理器將做爲一種相對複雜些的實現存在,處理的邏輯也是各有千秋。此處僅給一點點提示,你們可按需實現便可。

 

    
import com.my.mvc.app.common.helper.parser.*;

import java.util.Iterator;

/**
 * 功能描述: 自定義函數實現示例
 */
public class SimpleUdfAstHandler
        extends CommonConditionAstBranch
        implements SyntaxStatement {

    public SimpleUdfAstHandler(TokenDescriptor masterToken,
                                 Iterator<TokenDescriptor> candidates,
                                 TokenTypeEnum tokenType) {
        super(masterToken, candidates, tokenType);
    }

    @Override
    protected int getFixedExtTokenNum() {
        // 固定額外參數
        return 2;
    }

    @Override
    public String translateTo(TargetDialectTypeEnum targetSqlType) {
        // 自行實現
        String usage = masterToken.getRawWord();
        int paramStart = usage.indexOf('(');
        StringBuilder fieldBuilder = new StringBuilder();
        for (int i = paramStart; i < usage.length(); i++) {
            char ch = usage.charAt(i);
            if(ch == ' ') {
                continue;
            }
            if(ch == '$') {
                // 示例解析,只需一個id參數處理
                fieldBuilder.append(ch);
                while (++i < usage.length()) {
                    ch = usage.charAt(i);
                    if(ch >= '0' && ch <= '9') {
                        fieldBuilder.append(ch);
                        continue;
                    }
                    break;
                }
                break;
            }
        }
        String separator = " ";
        StringBuilder resultBuilder
                = new StringBuilder(fieldBuilder.toString())
                    .append(separator);
        // 根據各目標語言須要,作特別處理
        switch (targetSqlType) {
            case ES:
            case HIVE:
            case SPARK:
            case PRESTO:
            case RAW:
                extendTokens.forEach(r -> resultBuilder.append(r.getRawWord()).append(separator));
                return resultBuilder.toString();
        }
        throw new RuntimeException("unknown target dialect");
    }
}

  udf 做爲一個重點處理對象,你們按需實現便可。

 

8. 自定義關鍵字的解析實現

  自定義關鍵字的目的,也許是爲了讓用戶使用更方便,也許是爲了理解更容易,也許是爲系統處理方便,但它與udf實際有殊途同歸之妙,不過自定義關鍵字能夠儘可能定義得簡單些,這也從另外一個角度將其與udf區分開來。所以,咱們能夠將關鍵字處理概括爲一類處理器,簡化實現。

import com.my.mvc.app.common.helper.parser.*;
import com.my.mvc.app.common.util.ClassLoadUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 功能描述: 系統自定義常量解析類
 *
 */
@Slf4j
public class SysCustomKeywordAstHandler
        extends CommonConditionAstBranch
        implements SyntaxStatement {

    private static final Map<String, SysKeywordDefiner>
            keywordDefinerContainer = new ConcurrentHashMap<>();

    static {
        try {
            // 自動發現加載指定路徑下全部關鍵字解析器 keyword 子包
            String currentPackage = SysCustomKeywordAstHandler.class.getPackage().getName();
            ClassLoadUtil.loadPackageClasses(
                    currentPackage + ".custom");
        }
        catch (Throwable e) {
            log.error("加載包路徑下文件失敗", e);
        }
    }

    public SysCustomKeywordAstHandler(TokenDescriptor masterToken,
                                      Iterator<TokenDescriptor> candidates,
                                      TokenTypeEnum tokenType) {
        super(masterToken, candidates, tokenType);
    }

    @Override
    public String translateTo(TargetDialectTypeEnum targetSqlType) {
        String usage = masterToken.getRawWord();
        String keywordName = parseSysKeywordName(usage);
        SysKeywordDefiner definer = getKeywordDefiner(keywordName);
        List<TokenDescriptor> mergedToken = new ArrayList<>(extendTokens);
        mergedToken.add(0, masterToken);
        if(definer == null) {
//            throw new BizException("不支持的關鍵字: " + keywordName);
            // 在未徹底替換全部關鍵字功能以前,不得拋出以上異常
            log.warn("系統關鍵字[{}]定義未找到,降級使用原始語句,請儘快補充功能.", keywordName);
            return translateToDefaultRaw(mergedToken);
        }
        return definer.translate(mergedToken, targetSqlType);
    }

    /**
     * 獲取關鍵字名稱
     *
     * 檢測關鍵詞是不是 '%%#{datepart}%' 格式的字符
     * @return 關鍵字標識如 datepart
     */
    private String parseSysKeywordName(String usage) {
        if('\'' == usage.charAt(0)) {
            String keywordName = getSysKeywordNameWithPreLikeStr(usage);
            if(keywordName == SYS_CUSTOM_EMPTY_KEYWORD_NAME) {
                throw new RuntimeException("系統關鍵詞定義非法, 請以 #{} 使用關鍵詞2");
            }
            return keywordName;
        }
        return getSysKeywordNameNormal(usage);
    }

    private static final String SYS_CUSTOM_EMPTY_KEYWORD_NAME = "";

    /**
     * 獲取關鍵字名稱('%#{datepart}%')
     *
     * @param usage 完整用法
     * @return 關鍵字名稱 如 datepart
     */
    public static String getSysKeywordNameWithPreLikeStr(String usage) {
        if('\'' != usage.charAt(0)) {
            return SYS_CUSTOM_EMPTY_KEYWORD_NAME;
        }
        StringBuilder keywordBuilder = new StringBuilder();
        int preLikeCharNum = 0;
        String separatorChars = " -+(){}[],";
        for (int i = 1; i < usage.length(); i++) {
            char ch = usage.charAt(i);
            if(ch == '%') {
                preLikeCharNum++;
                continue;
            }
            if(ch != '#'
                    || usage.charAt(++i) != '{') {
                return SYS_CUSTOM_EMPTY_KEYWORD_NAME;
            }

            while (++i < usage.length()) {
                ch = usage.charAt(i);
                keywordBuilder.append(ch);
                if(i + 1 < usage.length()) {
                    char nextChar = usage.charAt(i + 1);
                    if(separatorChars.indexOf(nextChar) != -1) {
                        break;
                    }
                }
            }
            break;
        }
        return keywordBuilder.length() == 0
                ? SYS_CUSTOM_EMPTY_KEYWORD_NAME
                : keywordBuilder.toString();
    }


    /**
     * 解析關鍵詞特別用法法爲一個個token
     *
     * @param usage 原始使用方式如: #{day+1}
     * @param prefix 字符開頭
     * @param suffix 字符結尾
     * @return 拆分後的token, 已去除分界符 #{}
     */
    public static List<TokenDescriptor> parseSysCustomKeywordInnerTokens(String usage,
                                                                         String prefix,
                                                                         String suffix) {
//        String prefix = "#{day";
//        String suffix = "}";
        String separatorChars = " ,{}()[]-+";
        if (!usage.startsWith(prefix)
                || !usage.endsWith(suffix)) {
            throw new RuntimeException("關鍵字使用格式不正確: " + usage);
        }
        List<TokenDescriptor> innerTokens = new ArrayList<>(2);
        TokenDescriptor token;
        for (int i = prefix.length();
             i < usage.length() - suffix.length(); i++) {
            char ch = usage.charAt(i);
            if (ch == ' ') {
                continue;
            }
            if (ch == '}') {
                break;
            }
            if (ch == '-' || ch == '+') {
                token = new TokenDescriptor(ch, TokenTypeEnum.SIMPLE_MATH_OPERATOR);
                innerTokens.add(token);
                continue;
            }
            StringBuilder wordBuilder = new StringBuilder();
            do {
                ch = usage.charAt(i);
                wordBuilder.append(ch);
                if (i + 1 < usage.length()) {
                    char nextChar = usage.charAt(i + 1);
                    if (separatorChars.indexOf(nextChar) != -1) {
                        break;
                    }
                    ++i;
                }
            } while (i < usage.length());
            String word = wordBuilder.toString();
            TokenTypeEnum tokenType = TokenTypeEnum.WORD_STRING;
            if(StringUtils.isNumeric(word)) {
                tokenType = TokenTypeEnum.WORD_NUMBER;
            }
            innerTokens.add(new TokenDescriptor(wordBuilder.toString(), tokenType));
        }
        return innerTokens;
    }

    /**
     * 解析普通關鍵字定義 #{day+1}
     *
     * @return 關鍵字如: day
     */
    public static String getSysKeywordNameNormal(String usage) {
        if(!usage.startsWith("#{")) {
            throw new RuntimeException("系統關鍵詞定義非法, 請以 #{} 使用關鍵詞");
        }
        StringBuilder keywordBuilder = new StringBuilder();
        for (int i = 2; i < usage.length(); i++) {
            char ch = usage.charAt(i);
            if(ch == ' ' || ch == ','
                    || ch == '+' || ch == '-'
                    || ch == '(' || ch == ')' ) {
                break;
            }
            keywordBuilder.append(ch);
        }
        return keywordBuilder.toString();
    }
    /**
     * 默認使用原始語句返回()
     *
     * @return 原始關鍵字詞組
     */
    private String translateToDefaultRaw(List<TokenDescriptor> tokens) {
        String separator = " ";
        StringBuilder sb = new StringBuilder();
        tokens.forEach(r -> sb.append(r.getRawWord()).append(separator));
        return sb.toString();
    }

    /**
     * 獲取關鍵詞定義處理器
     *
     */
    private SysKeywordDefiner getKeywordDefiner(String keyword) {
        return keywordDefinerContainer.get(keyword);
    }

    /**
     * 註冊新的關鍵詞
     *
     * @param definer 詞定義器
     * @param keywordNames 關鍵詞別名(支持多個,目前只有一個的場景)
     */
    public static void registerDefiner(SysKeywordDefiner definer, String... keywordNames) {
        for (String key : keywordNames) {
            keywordDefinerContainer.putIfAbsent(key, definer);
        }
    }
}

// ----------- SysKeywordDefiner ------------------    
import com.my.mvc.app.common.helper.parser.TargetDialectTypeEnum;
import com.my.mvc.app.common.helper.parser.TokenDescriptor;

import java.util.List;

/**
 * 功能描述: 系統關鍵詞定義接口
 *
 *      (關鍵詞通常被自動註冊,無需另外調用)
 *      關鍵詞名稱,如: day, dd, ddpart ...
 *      day
 *      '%#{datepart}%'
 *
 */
public interface SysKeywordDefiner {

    /**
     * 轉換成目標語言表示
     *
     *
     *
     * @param tokens 全部必要詞組
     * @param targetSqlType 目標語言類型 es|hive|presto|spark
     * @return 翻譯後的語言表示
     */
    String translate(List<TokenDescriptor> tokens,
                     TargetDialectTypeEnum targetSqlType);

}


// ----------- SyntaxStatement ------------------    

import com.my.mvc.app.common.helper.parser.TargetDialectTypeEnum;
import com.my.mvc.app.common.helper.parser.TokenDescriptor;
import com.my.mvc.app.common.helper.parser.keyword.SysCustomKeywordAstHandler;
import com.my.mvc.app.common.helper.parser.keyword.SysKeywordDefiner;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
 * 功能描述: day 關鍵詞定義
 *
 *      翻譯當天日期,作相應運算
 *
 */
public class DayDefinerImpl implements SysKeywordDefiner {

    private static final String KEYWORD_NAME = "day";

    static {
        // 自動註冊關鍵詞到系統中
        SysCustomKeywordAstHandler.registerDefiner(new DayDefinerImpl(), KEYWORD_NAME);
    }

    @Override
    public String translate(List<TokenDescriptor> tokens,
                            TargetDialectTypeEnum targetSqlType) {
        String separator = " ";
        String usage = tokens.get(0).getRawWord();
        List<TokenDescriptor> innerTokens = SysCustomKeywordAstHandler
                .parseSysCustomKeywordInnerTokens(usage, "#{", "}");
        switch (targetSqlType) {
            case ES:
            case SPARK:
            case HIVE:
            case PRESTO:
                int dayAmount = 0;
                if(innerTokens.size() > 1) {
                    String comparator = innerTokens.get(1).getRawWord();
                    switch (comparator) {
                        case "-":
                            dayAmount = -Integer.valueOf(innerTokens.get(2).getRawWord());
                            break;
                        case "+":
                            dayAmount = Integer.valueOf(innerTokens.get(2).getRawWord());
                            break;
                        default:
                            throw new RuntimeException("day關鍵字不支持的操做符: " + comparator);
                    }
                }
                // 此處格式可能須要由外部傳入,配置化
                return "'"
                        + LocalDate.now().plusDays(dayAmount)
                        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
                        + "'" + separator;
            case RAW:
            default:
                StringBuilder sb = new StringBuilder();
                tokens.forEach(r -> sb.append(r.getRawWord()).append(separator));
                return sb.toString();
        }
    }

}

  關鍵詞的處理,值得一提是,使用了一個橋接類,且自動發現相應的實現。(可參考JDBC的 DriverManager 的實現) 從而在實現各關鍵字後,直接放入相應包路徑,便可生效。還算優雅吧。

 

9. 單元測試

  最後一部分,實際也是很是重要的部分,被我簡單化了。咱們應該根據具體場景,羅列全部可能的狀況,以知足全部語義,單測經過。樣例以下:

import com.my.mvc.app.common.helper.SimpleSyntaxParser;
import com.my.mvc.app.common.helper.parser.ParsedClauseAst;
import com.my.mvc.app.common.helper.parser.TargetDialectTypeEnum;
import org.junit.Assert;
import org.junit.Test;

public class SimpleSyntaxParserTest {

    @Test
    public void testParse1() {
        String rawClause = "$15573 = 123 and (week_diff($123568, $82949) = 1) or $39741 = #{day+1} and week_diff($35289) = -1";
        ParsedClauseAst clauseAst = SimpleSyntaxParser.parse(rawClause);
        Assert.assertEquals("解析成目標語言ES不正確",
                "$15573 = 123 and ( $123568 = 1 ) or $39741 = '2020-10-07' and $35289 = -1",
                    clauseAst.translateTo(TargetDialectTypeEnum.ES));
    }
}

  以上,就是一個完整地、簡單的語法解析器的實現了。也許各自場景不一樣,但相信思想老是相通的。

相關文章
相關標籤/搜索