我正在参加「掘金·启航计划」
前言
最近项目上需要统计程序中业务对应模块实际在用的表名,并输出一个可视化 excel 文件。
首先拆分任务:
- 扫描对应业务模块的表
- 写成 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 太复杂,比如说很多标签,并且各种嵌套,就没法处理,只能找到一个覆盖面比较大的正则替换。如下代码所示:
- 先走正常的 druid 解析逻辑,如果报错
- 再通过正则解析
/**
* (?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 的解析。
完整代码: