有時候,咱們爲了屏蔽一些底層的差別,咱們會要求上游系統按照某種約定進行傳參。而在咱們本身的系統層則會按照具體的底層協議進行適配,這是通用的作法。但當咱們要求上游系統傳入的參數很是複雜時,也許咱們會有一套本身的語法定義,用以減輕全部參數的不停變化。好比sql協議,就是一個一級棒的語法,一樣是調用底層功能,但它能夠很方便地讓用戶傳入任意的參數。java
若是咱們本身可以實現一套相似的東西,想來應該蠻有意思的。算法
不過,咱們徹底沒有必要要實現一整套完整的東西,咱們只是要體驗下這個語法解析的過程,那就好辦了。本文就來給個簡單的解析示例,供看官們參考。sql
目標:數據庫
基於咱們自定義的一套語法,咱們要實現一套相似於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
實際上整個看下來,和一道普通的算法題差不太多呢。但實際想要完整實現這麼個小東西,也是要費很多精力的。函數
咱們要作一個解析器,或者說翻譯器,首先第一步,天然是要從根本上理解原語義,而後再根據目標語言的表達方式,轉換過去就能夠了。
若是你們有看過一些編譯原理方面的書,就應該知道,整個編譯流程大概分爲: 詞法分析;語法分析;語義分析;中間代碼生成;代碼優化;目標代碼; 這麼幾個過程,而每一個過程每每又是很是複雜的,而最複雜的每每又是其上下文關係。不過,咱們不想搞得那麼複雜(也搞不了)。
雖然咱們不像作一個編譯器同樣複雜,但咱們仍然能夠參考其流程,能夠爲咱們提供比較好的思路。
咱們就主要作3步就能夠了:1. 分詞;2. 語義分析; 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, 由於還有很是多的細節。
咱們須要爲每個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... } }
如上,基本能夠描述各詞的類型了,若是不夠,咱們能夠視狀況新增便可。從這裏,咱們能夠準確地看出一些分詞的規則。
很明顯,咱們須要一個統籌全部可被處理的詞組的地方,這就是符號表,咱們能夠經過符號表,準確的查到哪些是系統關鍵詞,哪些是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 + '\'' + '}'; } }
系統可支持的目標語言是有限的,應當將其定義爲枚舉類型,以便用戶規範使用。
/** * 功能描述: 組合標籤可被翻譯成的 方言枚舉 * */ public enum TargetDialectTypeEnum { ES, HIVE, PRESTO, SPARK, /** * 原始語句 */ RAW, ; }
若是有一天,你新增了一個語言的實現,那你就能夠將類型加上,這樣用戶也就能夠調用了。
解析器的幾大核心之一就是詞義處理器,前面不少的工做都是準備性質的,好比分詞,定義等。前面也看到,咱們將詞義處理器統必定義了一個接口: 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 做爲一個重點處理對象,你們按需實現便可。
自定義關鍵字的目的,也許是爲了讓用戶使用更方便,也許是爲了理解更容易,也許是爲系統處理方便,但它與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 的實現) 從而在實現各關鍵字後,直接放入相應包路徑,便可生效。還算優雅吧。
最後一部分,實際也是很是重要的部分,被我簡單化了。咱們應該根據具體場景,羅列全部可能的狀況,以知足全部語義,單測經過。樣例以下:
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)); } }
以上,就是一個完整地、簡單的語法解析器的實現了。也許各自場景不一樣,但相信思想老是相通的。