1. 程式人生 > >解析xml檔案動態拼接sql語句

解析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模板引擎來實現的,效率很高。