扫描 xml 中所有的表名并生成 excel 文件

337 阅读3分钟

我正在参加「掘金·启航计划」

前言

最近项目上需要统计程序中业务对应模块实际在用的表名,并输出一个可视化 excel 文件。

首先拆分任务:

  1. 扫描对应业务模块的表
  2. 写成 excel 文件输出

文件输出的可暂时不管,做 CRUD 的,文件下载也绝不会少,无非就是把 List 装换为指定的excel 内容格式,使得内容更加可视化,满足领导的需求。

实战

分析

基本上一个模块对应一个入口(Controller),我们可以从 Controller 入手,获取属性字段(即 service 接口)再获取它的 impl 实现类,再通过实现类的属性字段获取 Mapper 文件,再通过 Mapper 文件名 + “.xml”就获得了 mapper.xml 文件,然后就对其遍历每个 SQL 语句,解析获取其中的表名。

涉及到的主要知识点:

  • 反射
  • druid 解析 SQL
  • 正则解析 SQL
  • xml 解析(这里直接使用 java 自带的,避免引用过多的外部包,当然 dom4j 也可以的)

代码

controller 处理

class.getDeclaredFields() 获取所有属性,对其进行遍历:一个 Controller 可能有多个 service

Class controllerClass = TestController.class;
Field[] declaredFields = controllerClass.getDeclaredFields();
for (Field field : declaredFields) {
    Class<?> tClass = field.getType(); // service 接口

    //使用 Spring 中注册的 Bean 获取 service 的实现类
    Map<String, ?> beansOfType = applicationContext.getBeansOfType(tClass);

    if (beansOfType.isEmpty()) {   //没有实现类直接跳过
        continue;
    }
    Object bean = beansOfType.values().iterator().next();
}

实现类处理

这里我碰到了不一样的情况,一般我们都是使用 @Service 注解注入实现类,但是程序中发现了一个使用 @Repository 注解注入实现类的。

@Repository 注解是通过 CGLIB 注入的

@Service 注解

所以下面的代码才会分别处理:

if (bean.getClass().getName().contains("$$")) { //cglib 代理
    logger.info("cglib 代理:");
    //                        String simpleName = bean.getClass().getSimpleName();
    Constructor<?>[] declaredConstructors = bean.getClass().getDeclaredConstructors();
    for (int j = 0; j < declaredConstructors.length; j++) {
        Class<?>[] parameterTypes = declaredConstructors[j].getParameterTypes();
        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> implClass = parameterTypes[i];  //这里已经是 mapper
            dealMapper(implClass.getSimpleName(), set);
        }
    }
} else { //jdk动态代理
    Class<?> implClass = bean.getClass(); //这是实现类
    Field[] mapperFields = implClass.getDeclaredFields();
    for (Field mapperField : mapperFields) {
        String mapperClassName = mapperField.getType().getSimpleName();
        dealMapper(mapperClassName, set);
    }
}

mapper.xml 处理

最后获取到 Mapper 接口文件,后面拼接上 .xml 就是需要解析的xml文件。

读取文件:

PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
String locationPattern = "classpath*:mapper/**/" + mapperXml;
File xmlFile = null;
try {
    Resource[] resources = resolver.getResources(locationPattern);
    xmlFile = resources[0].getFile();
} catch (Exception e) {
    logger.error("没有找到对应的 xml 文件:{}", mapperXml);
}

解析文件:

下面是对一个 xml 文件中的所有 select 标签 循环遍历取得其中的内容。

DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document document = dBuilder.parse(xmlFile);

NodeList nodeListSelect = document.getElementsByTagName("select");
 for (int i = 0; i < nodeListSelect.getLength(); i++) {
        Node node = nodeListSelect.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            Element element = (Element) node;

            String textContent = element.getTextContent();
        }}

这里注意一个问题:element.getTextContent() 获取的内容会把里面包含的标签默认去掉,而 druid 只能解析完整的 sql 语句。

所以这里会做一些简单的替换:比如 < where> 、< choose >、< if> 标签等。

    NodeList where = element.getElementsByTagName("where");
    for (int j = 0; j < where.getLength(); j++) {
        String textContent1 = where.item(j).getTextContent();
        textContent = textContent.replace(textContent1, "where " + textContent1);
    }
//                                    logger.info("where 解析后的 SQL:{}", textContent);

    //去除 choose 标签 下的 when 标签(保留一个)
    NodeList choose = element.getElementsByTagName("choose");
    for (int j = 0; j < choose.getLength(); j++) {
        Node nodechoose = choose.item(j);
        String chooseText = nodechoose.getTextContent();

        Element elementchoose = (Element) nodechoose;
        NodeList when = elementchoose.getElementsByTagName("when");
        String textContent2 = when.item(0).getTextContent();
//                                            String textContent1 =nodechoose.getTextContent();
        textContent = textContent.replace(chooseText, textContent2);
    }
   NodeList ifNodeList = element.getElementsByTagName("if");
    for (int j = 0; j < ifNodeList.getLength(); j++) {
        String textContent1 = ifNodeList.item(j).getTextContent();
        textContent = textContent.replace(textContent1, "");
    }

sql 解析获取表名

下面先是使用 druid 解析 SQL 语句,如果一个SQL 太复杂,比如说很多标签,并且各种嵌套,就没法处理,只能找到一个覆盖面比较大的正则替换。如下代码所示:

  1. 先走正常的 druid 解析逻辑,如果报错
  2. 再通过正则解析
 /**
     * (?i) 表示忽略大小写匹配
     * (join|from) 匹配 JOIN 或 FROM 关键字
     * \s+ 匹配一个或多个空白字符(包括空格、制表符、换行符等)
     * (\s*,\s*\w+)* 匹配零个或多个以逗号分隔的表名,逗号前后可以有任意数量的空白字符
     * ((\w+.)?\w+) 后面跟着至少一个空格,然后是一个形如schema.table的表名 或者直接表名。
     */
String regex = "(?i)(from|join|update|delete)\s+((\w+\.)?\w+)(\s*,\s*\w+)*";
String dbType = JdbcConstants.MYSQL;
try {
    //格式化输出
    //            String sqlResult = SQLUtils.format(sql, dbType,SQLUtils.DEFAULT_FORMAT_OPTION);
    //            sql = sql.replaceAll("<where>", "where").replaceAll("</where>", "");
    List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
    if (CollectionUtils.isEmpty(stmtList)) {
        logger.info("stmtList为空无需获取");
        return;
    }
    for (SQLStatement sqlStatement : stmtList) {
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
        sqlStatement.accept(visitor);
        Map<TableStat.Name, TableStat> tables = visitor.getTables();
        Set<TableStat.Name> tableNameSet = tables.keySet();
        for (TableStat.Name name : tableNameSet) {
            String tableName = name.getName();
            if (StringUtils.isNotBlank(tableName) && !tableName.startsWith("pm.") && !tableName.startsWith("ndm.")) {
                //                        tableNameList.add(tableName);
                set.add(tableName.toUpperCase(Locale.ENGLISH).trim());
            }
        }
    }
} catch (Exception e) {
    logger.info("xml id 为:{}", element.getAttribute("id"));
    //            logger.error("**************异常SQL:[{}]*****************\n", sql);
    //            logger.error("使用 druid 解析失败,使用 正则匹配");
    Matcher matcher = pattern.matcher(sql);
    while (matcher.find()) {
        //                logger.info("正则解析 sql 后的表名:[{}]", matcher.group(2));
        if (matcher.group(2).length() > 6 && !matcher.group(2).startsWith("pm.") && !matcher.group(2).startsWith("ndm.")) {
            //                    tableNameList.add(matcher.group(2));
            set.add(matcher.group(2).toUpperCase(Locale.ENGLISH).trim());
        }
    }
}

生成 excel

将数据解析为 Module、Table 实体类对应的格式,使用 easyPOI 工具包对其生成

class Module {
    @Excel(name = "模块名称", needMerge = true, width = 20)
    private String moduleName;
    @ExcelCollection(name = "")
    private List<Table> tableList;
}

class Table {
    @Excel(name = "模块对应的表", width = 50)
    private String tableName;
}

ExportParams exportParams = new ExportParams();
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, Module.class, list);

try {
    FileOutputStream fout = new FileOutputStream("D:/模块对应表名.xls");
    workbook.write(fout);
    fout.close();
} catch (Exception e) {
    e.printStackTrace();
}

最终的excel 可视化图表:

总结

  • 上面的代码非强依赖性,只需要知道你模块对应的 Controller 就能输出上面格式的 Excel 只有一个点需要注意:在我们获取 xml 文件的时候,使用的是 Mapper 文件名直接拼接的 .xml 。一般来说,开发都是这样命名文件的,如果非得两个不一致,那就没办法了。(写死有点不优雅)
  • 上面涉及的知识点有:反射、Spring 容器 Bean 的加载、XML 的处理、druid 的 sql 解析、正则对 sql 的解析。

完整代码:

gitee.com/ncharming/k…