现在有很多开源的持久层框架,比如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模板引擎来实现的,效率很高。