最近做了一道题目,记录一下实现思路。
题目大概是:
目前JAVA程序操作数据库主要通过两种方式:
(1)JDBC:直接在JAVA源码中写SQL语句
(2)MyBatis:在XML中配置SQL语句或通过注解指定SQL语句
输入为一个JAVA源码文件zip包,统计出访问的数据库表名称以及操作类型(SELECT/UPDATE/DELETE), 其中插入insert操作归为UPDATE进行输出。最终将统计结果输出到一个txt文件,一行就是一个结果:
文件名 表名 操作类型(query/update/delete)
当时的解题思路:
这主要是能够对每个文件进行解析,利用正则表达式来匹配出大概的字符串,然后再做处理。
(当时因为工作繁忙,没啥时间,所以主要花时间在解析xml上,Java有些注解类的用法还没有时间去弄)
1. 程序入口,读取zip文件
利用java.util.zip包,将指定目录下的zip文件进行解压,拿到ZipEntry实体类,就是文件对象。过滤掉一些没用或者非法的文件,比如去掉文件夹,去掉后缀不是java或xml的文件,去掉/target目录下的文件,避免重复统计。
2. 通过第1步,拿到需要解析的文件InputStream流,这里区分java文件和xml文件
2.1 对于xml文件
(1)解析xml文件
通过dom4j解析整个xml文件,这里首先要去除dtd校验
SAXReader saxReader = new SAXReader();
// 去除dtd校验
saxReader.setValidation(false);
saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
解析出xml的节点后,依次获取我们需要解析的标签来处理,这里拿的标签是<select><SELECT><update><UPDATE><delete><DELETE><insert><INSERT>,区分大小写。
(2)通过(1)拿到了对应标签的内容,是数组List<Element> childList
1》select标签(SELECT标签相同):
一般xml中select标签的格式是:
<select id="getById" parameterType="..." resultType="map">
select * from table where a = b;
</select>
通过遍历childList, 调用getTextTrim方法,可以拿到标签内去掉空格后的字符串
for (Element e : childList) {
// 拿到具体要解析的字符串:select * from table where a = b;
String str = e.getTextTrim();
}
这里可以归纳出一个算法:
第1步:对于一条完整的select sql查询语句,从上到下遍历整个字符串sqlStr,维持两个索引值:开始索引startIndex和结束索引endIndex。
第2步:以开始索引startIndex为起点,找到第一个遇到的结束标签(即表示一系列表声明的结束,比如WHERE、where、INNER、inner、LEFT、left、RIGHT、right、ON、on)的位置,或者是sqlStr的末尾(表示没有任何结束标签),将该标签位置设置为结束索引endIndex。
第3步:根据上一步结束索引的位置,找到离结束索引endIndex最近的一个开始标签(即表示一系列表声明的开始,比如FROM、from、JOIN、join)的位置,将该标签位置设置为开始索引startIndex。
第4步:截取开始索引和结束索引之间的字符串,设为tableStr。
第5步:分析tableStr,获取表名,一般是以逗号“,”分隔,解析出表名+空格+昵称,然后再以空格分隔获取表名。
第6步:以上次的结束索引 endIndex值,作为新的一轮的开始索引startIndex值,继续循环上面的分析,直到开始索引startIndex大于整个sql字符串sqlStr的长度。
算法的伪代码:
public List<String> getAllTableStrByStartEndIndex(String fullSqlStr) {
List<String> allSelTableStrs = new ArrayList<>();
// 开始索引
int startIndex = 0;
// 结束索引
int endIndex = 0;
while (startIndex < fullSqlStr.length()) {
// 先找结束标签的位置
MinEndIndexTag minEndIndexTag = findEndIndexFromStartIndex(fullSqlStr, startIndex);
endIndex = minEndIndexTag.getEndIndex();
if (endIndex == startIndex) {
break;
}
// 当结束标签找不到时,就处理剩下的
if (endIndex == -1) {
endIndex = fullSqlStr.length();
}
// 根据结束标签,找到最后一个开始标签
startIndex = findLastStartIndexFromStartIndex(fullSqlStr, startIndex, endIndex);
if (startIndex == -1) {
// 如果找不到开始标签,则将开始标签设置为(结束标签+结束标签的长度)
startIndex = endIndex + minEndIndexTag.getEndIndexTag().length();
} else {
// 获取tableStr
String tableStr = fullSqlStr.substring(startIndex, endIndex);
if (tableStr != null && !"".equals(tableStr)) {
allSelTableStrs.add(tableStr);
// 开始标签重新赋值,也是(结束标签+结束标签的长度)
startIndex = endIndex + minEndIndexTag.getEndIndexTag().length();
} else {
break;
}
}
}
}
具体的获取开始标签和结束标签的函数就不列举了。
通过上面的算法,能够获取到一个表名字符串的数组,里面的表名字符串就类似于:
tableA a, tableB b
这样就比较好处理了,先以逗号分隔,再以空格分隔,获取到真正的表名。
2》update标签(UPDATE标签相同)
update标签的做法比较简单,没有select标签的情况复杂,只需要用一个正则表达式就可以解析多个表名字符串tableStr,然后再做同样的表名解析。
String XML_UPDATE = "UPDATE\\s*(\\w*\\.*\\w*)\\s*SET";
// 忽略大小写
Pattern pattern = Pattern.compile(XML_UPDATE, Parrern.CASE_INSENSITIVE);
3》delete标签(DELETE标签相同)
与update标签的做法一样,正则表达式为:
String XML_DELETE = "DELETE\\s*FROM\\s*(\\w*\\.*\\w*)";
4》insert标签(INSERT标签相同)
与update标签的做法一样,正则表达式为:
String XML_INSERT = "INSERT\\s*INTO\\s*(\\w*\\.*\\w*)";
至此,xml的sql语句就已经解析完成了,当然还有很多复杂的sql语句,需要针对性的解析,处理一些特殊情况,不过整体按照上面的算法思路来处理应该是OK的。