解析xml檔案動態拼接sql語句
現在有很多開源的持久層框架,比如Mybatis、BeetlSQL、Hibernate、DbUtils。當我們需要自己手寫sql的時候。使用Mybatis、BeetlSQL(這個個人更喜歡,因為結合了hibernate和mybatis各自的優點)框架相對來說更好,因為它將sql 放到配置檔案裡面。而不是硬編碼到程式碼中。使用了這麼多框架,如果想程式設計思想更上一層,不僅要怎麼使用,還要學習其實現原理。接下來,自己來實現 解析xml檔案 來 動態拼接 得到 sql。
1、問題引入?
現在我想從下面的xml配置檔案中,根據id得到正確的sql。裡面的if、elseif、else這幾個標籤要可以實現條件判斷的功能。該如何實現呢? 有點類似mybatis。
檔名:test3.xml, 如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE sqls[ <!ELEMENT sqls ( sql* ) > <!ELEMENT sql ( #PCDATA | if | else | elseif )*> <!ATTLIST sql id ID #REQUIRED> <!ELEMENT if ( #PCDATA | if | else | elseif )* > <!ATTLIST if test CDATA #REQUIRED> <!ELEMENT elseif ( #PCDATA | if | else | elseif )* > <!ATTLIST elseif test CDATA #REQUIRED> <!ELEMENT else ( #PCDATA | if | else | elseif )* > ]> <sqls> <sql id="queryUser"> select * from user where name = 'chenjiahao' <if test="age gt 18"> and age >:age </if> <elseif test="age lt 18"> <![CDATA[ and age <:age ]]> </elseif> <else> and age=:age </else> order by create_date desc </sql> </sqls>
2、實現思路:
(1)不用條件判斷的情況下:
無需拼接,直接根據id獲取到指定sql。(通過解析xml技術很容易實現)
(2)有條件判斷的情況下:
首先根據id獲取到指定的元素,再遍歷子元素(遞迴思想,如果還有子元素則遞迴),通過邏輯判斷,動態拼接成最終的sql。如果if elseif 標籤中屬性test=" 表示式 "的結果為true就拼接,否則跳過。
如何知道test="表示式"的值為真呢?可以藉助第三方開源專案——表示式引擎來幫我們解決。Fel表示式計算引擎快速上手
3、使用到的技術
dom4j、Fel表示式計算引擎。
4、自己簡單實現示例程式碼如下:
dom4j 和 fel jar依賴:
<!-- https://mvnrepository.com/artifact/org.dom4j/dom4j -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.0</version>
</dependency>
<!--fel 表示式引擎-->
<dependency>
<groupId>org.eweb4j</groupId>
<artifactId>fel</artifactId>
<version>0.8</version>
</dependency>
package com.cjh.test.xml;
import com.greenpineyu.fel.FelEngine;
import com.greenpineyu.fel.FelEngineImpl;
import com.greenpineyu.fel.context.FelContext;
import com.greenpineyu.fel.context.MapContext;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 解析xml sql 動態拼接生成
* @author chen jia hao
*/
public class XmlTest {
private static Map<String, String> keywordMap = new HashMap<>();
private static Map<String,Document> sqlDocumentMap = new HashMap<>();
private static Map<String, Object> params = new HashMap<>();
private static FelEngine felEngine = new FelEngineImpl();
private static FelContext mapContext = new MapContext();
static{
keywordMap.put("if","if");
keywordMap.put("elseif","elseif");
keywordMap.put("else","else");
keywordMap.put("sql","sql");
keywordMap.put("sqls","sqls");
keywordMap.put("eq","==");
keywordMap.put("lt","<");
keywordMap.put("gt",">");
keywordMap.put("le","<=");
keywordMap.put("ge",">=");
}
public static void main(String[] args) {
test();
}
public static void test(){
StringBuffer sqlBuffer = new StringBuffer();
try {
long startTime = System.currentTimeMillis();
//設定引數
Map<String, Object> data = new HashMap<>();
data.put("age",18);
setParams(data);
//解析sql
Element sqlElement = getSqlElement("test3.queryUser");
parseSql(sqlBuffer,sqlElement);
String sqlText = sqlBuffer.toString().trim();
System.out.println("配置檔案原sql:"+sqlText);
System.out.println("預處理sql語句:"+getSql(sqlText));
List<String> sqlParameterNames = getSqlParameterNames(sqlText);
boolean isFirst = true;
System.out.print("引數:{");
for(Map.Entry<String,Object> item: params.entrySet()){
if(isFirst){
isFirst = false;
}else{
System.out.print(",");
}
System.out.print(item.getKey()+":"+item.getValue());
}
System.out.println("}");
long endTime = System.currentTimeMillis();
System.out.println("耗時:"+(endTime-startTime)+"ms");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根據name讀取指定document文件,相對於classpath路徑
* @author chen jia hao
* @param name
* @return
*/
public static Document getDocument(String name){
if(sqlDocumentMap.get(name)==null){
try {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(ClassLoader.getSystemResourceAsStream(name+".xml"));
sqlDocumentMap.put(name,document);
} catch (DocumentException e) {
e.printStackTrace();
}
}
return sqlDocumentMap.get(name);
}
/**
* 根據sqlId獲取指定sql元素
* @param sqlId
* @return
*/
public static Element getSqlElement(String sqlId){
String[] sqlIdArr = sqlId.split("\\.");
String sqlFileName = sqlIdArr[0];
String sql = sqlIdArr[1];
return (Element)getDocument(sqlFileName).selectSingleNode("//sql[@id='"+sql+"']");
}
/**
* 遞迴解析sql -- 核心部分
* @author chen jia hao
* @param sqlBuffer
* @param element
*/
public static void parseSql(StringBuffer sqlBuffer,Element element){
List<Node> tempList = element.content();
List<Node> nodeList = new ArrayList<>();
if( tempList!=null ){
//這裡排除空,因為換行也會當被xml當成子元素
tempList.stream().forEach(item->{
int length = item.getText().replaceAll("\\s", "").length();
if(length>0){
nodeList.add(item);
}
});
//標記:if、elseif、else 其中是否有一個為真
boolean preIfIsTrue = false;
for(int i=0,len=nodeList.size() ; i<len ;i++){
Node node = nodeList.get(i);
//文字型別或CDATA區,直接獲取sql
if(Node.TEXT_NODE == node.getNodeType() || Node.CDATA_SECTION_NODE == node.getNodeType()){
sqlBuffer.append(node.getText().replaceAll("\\s+"," "));
preIfIsTrue = false;
}else if(Node.ELEMENT_NODE == node.getNodeType()){
Element childElem = (Element)node;
String tagName = childElem.getName();
if("if".equals(tagName) ){
String test = childElem.attributeValue("test");
test = getEvalResult(test);
//如果為真,遞迴解析
if("true".equals(test)){
parseSql(sqlBuffer,childElem);
preIfIsTrue = true;
}
}else if("elseif".equals(tagName)){
if(i>0){
Node preNode = nodeList.get(i-1);
String preTagName = preNode.getName();
if( !preIfIsTrue && ("if".equals(preTagName) || "elseif".equals(preTagName) ) ){
String currTest = childElem.attributeValue("test");
currTest = getEvalResult(currTest);
//如果之前對應的if或elseif test為false,且當前test為真,則才遞迴解析
if("true".equals(currTest)){
parseSql(sqlBuffer,childElem);
preIfIsTrue = true;
}
}
}
}else if("else".equals(tagName) ){
if(i>0){
Node preNode = nodeList.get(i-1);
String preTagName = preNode.getName();
if( "if".equals(preTagName) || "elseif".equals(preTagName) ){
//如果之前對應的if或elseif為false,則才遞迴解析
if(!preIfIsTrue){
parseSql(sqlBuffer,childElem);
preIfIsTrue = false;
}
}
}
}
}
}
}
}
/**
* 生成預處理sql
* @author chen jia hao
* @param sqlText
* @return
*/
public static String getSql(String sqlText){
String reg = ":\\w+";
return sqlText.replaceAll(reg, "?");
}
/**
* 獲取引數名列表
* @author chen jia hoa
* @param sqlText
* @return
*/
public static List<String> getSqlParameterNames(String sqlText){
//獲取引數
List<String> paramNamesList = new ArrayList<>();
String reg = ":\\w+";
Pattern compile = Pattern.compile(reg);
Matcher matcher = compile.matcher(sqlText);
while(matcher.find()){
String group = matcher.group().substring(1);
paramNamesList.add(group);
}
return paramNamesList;
}
/**
* 計算表示式的結果
* @author chen jia hao
* @return "true" 或 "false"
*/
public static String getEvalResult(String exp){
if(exp!=null){
exp = exp.replaceAll(" eq "," == ").
replaceAll(" lt "," < ").
replaceAll(" le "," <= ").
replaceAll(" gt "," > ").
replaceAll(" ge "," >= ");
}
Object result = felEngine.eval(exp,mapContext);
return result.toString();
}
/**
* 設定引數
* @autor chen jia hao
*/
public static void setParams(Map<String,Object> data){
//引數
params = data;
}
}
列印結果:
解析效率上還是不高,這裡只是學習解析xml 動態拼接sql的一種思想。我們其實也可以換種思路去做。
比如:結合模板引擎(velocity、Beetl)來動態生成sql。Beetlsql就是基於Beetl模板引擎來實現的,效率很高。