解析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模板引擎来实现的,效率很高。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值