最近做了一道题目,记录一下实现思路。

题目大概是:

目前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的。