个人随笔
目录
SpringBoot+MyBatis实现可配置报表系统与Excel批量导入更新方案(传统模式)
2026-01-14 11:09:13

一、需求概述

核心需求:

  1. 可配置报表系统:报表查询SQL配置在数据库中,前端传递参数,需利用MyBatis预编译功能防止SQL注入,避免安全问题

  2. Excel批量导入/更新:导入目标表名、参数均为可配置,同时保证安全性和预编译特性

二、核心实现思路

核心原则:分离SQL模板静态部分与动态参数,MyBatis预编译处理参数,表名/字段名等配置项通过安全校验后拼接

  1. SQL模板设计:配置在数据库的SQL使用MyBatis风格参数占位符(如#{name}),保留预编译能力

  2. 动态SQL执行:通过MyBatis的SqlSession手动构建PreparedStatement,利用预编译处理参数

  3. 表名/字段名安全校验:对配置的表名、字段名做白名单校验,防止恶意注入

  4. Excel导入配置化:设计导入配置表,关联Excel列、目标表、字段映射关系,批量操作采用MyBatis批量语法

三、具体实现代码

3.1 数据库表设计(核心配置表)

  1. -- 报表SQL配置表
  2. CREATE TABLE report_config (
  3. id BIGINT PRIMARY KEY AUTO_INCREMENT,
  4. report_code VARCHAR(50) NOT NULL COMMENT '报表编码',
  5. sql_template TEXT NOT NULL COMMENT 'SQL模板(使用#{参数名}占位符)',
  6. create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
  7. update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  8. UNIQUE KEY uk_report_code (report_code)
  9. ) COMMENT '报表SQL配置表';
  10. -- Excel导入配置表
  11. CREATE TABLE excel_import_config (
  12. id BIGINT PRIMARY KEY AUTO_INCREMENT,
  13. import_code VARCHAR(50) NOT NULL COMMENT '导入编码',
  14. target_table VARCHAR(50) NOT NULL COMMENT '目标表名',
  15. column_mapping JSON NOT NULL COMMENT '列映射:{"excel列名":"数据库字段名"}',
  16. create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
  17. update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  18. UNIQUE KEY uk_import_code (import_code)
  19. ) COMMENT 'Excel导入配置表';
  20. -- 初始化测试数据
  21. INSERT INTO report_config (report_code, sql_template) VALUES
  22. ('user_query', 'SELECT * FROM user WHERE name = #{name} AND age = #{age}');
  23. INSERT INTO excel_import_config (import_code, target_table, column_mapping) VALUES
  24. ('user_import', 'user', '{"姓名":"name","年龄":"age","手机号":"phone"}');

3.2 核心依赖(pom.xml)

  1. <dependencies>
  2. <!-- SpringBoot核心 -->
  3. <dependency>
  4. <groupId>org.springframework.boot</groupId>
  5. <artifactId>spring-boot-starter-web&lt;/artifactId&gt;
  6. &lt;/dependency&gt;
  7. <!-- MyBatis -->
  8. <dependency>
  9. <groupId>org.mybatis.spring.boot</groupId>
  10. <artifactId>mybatis-spring-boot-starter</artifactId>
  11. <version>3.0.3</version>
  12. </dependency&gt;
  13. <!-- MySQL驱动 -->
  14. <dependency>
  15. <groupId>com.mysql</groupId>
  16. <artifactId>mysql-connector-j</artifactId>
  17. <scope>runtime</scope>
  18. </dependency>
  19. <!-- EasyExcel(Excel解析) -->
  20. <dependency>
  21. <groupId>com.alibaba</groupId>
  22. <artifactId>easyexcel</artifactId>
  23. <version>3.3.2</version>
  24. </dependency&gt;
  25. <!-- 工具类 -->
  26. <dependency>
  27. <groupId>cn.hutool</groupId>
  28. <artifactId>hutool-all</artifactId>
  29. <version>5.8.22</version>
  30. </dependency>
  31. </dependencies>

3.3 报表查询实现(利用MyBatis预编译)

  1. import org.apache.ibatis.session.SqlSession;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.jdbc.core.JdbcTemplate;
  4. import org.springframework.stereotype.Service;
  5. import org.springframework.util.Assert;
  6. import javax.sql.DataSource;
  7. import java.sql.Connection;
  8. import java.sql.PreparedStatement;
  9. import java.sql.ResultSet;
  10. import java.sql.SQLException;
  11. import java.util.ArrayList;
  12. import java.util.HashMap;
  13. import java.util.List;
  14. import java.util.Map;
  15. import java.util.regex.Matcher;
  16. import java.util.regex.Pattern;
  17. @Service
  18. public class ReportService {
  19. @Autowired
  20. private JdbcTemplate jdbcTemplate;
  21. @Autowired
  22. private SqlSession sqlSession;
  23. @Autowired
  24. private DataSource dataSource;
  25. /**
  26. * 执行配置化报表查询
  27. * @param reportCode 报表编码
  28. * @param params 前端传递的参数
  29. * @return 查询结果
  30. */
  31. public List<Map<String, Object>> executeReport(String reportCode, Map<String, Object> params) {
  32. // 1. 从数据库获取SQL模板
  33. String sqlTemplate = jdbcTemplate.queryForObject(
  34. "SELECT sql_template FROM report_config WHERE report_code = ?",
  35. new Object[]{reportCode},
  36. String.class
  37. );
  38. Assert.notNull(sqlTemplate, "报表配置不存在:" + reportCode);
  39. // 2. 利用MyBatis的预编译执行SQL(核心:保留预编译能力)
  40. try (Connection conn = dataSource.getConnection()) {
  41. // 构建预编译Statement
  42. PreparedStatement ps = createPreparedStatement(conn, sqlTemplate, params);
  43. // 执行查询
  44. ResultSet rs = ps.executeQuery();
  45. // 解析结果集为Map列表
  46. return ResultSetUtil.parseResultSet(rs);
  47. } catch (SQLException e) {
  48. throw new RuntimeException("执行报表SQL失败", e);
  49. }
  50. }
  51. /**
  52. * 创建预编译Statement(核心:参数预编译,防止注入)
  53. */
  54. private PreparedStatement createPreparedStatement(Connection conn, String sqlTemplate, Map<String, Object> params) throws SQLException {
  55. // 步骤1:解析SQL模板中的参数名,替换为?(保持MyBatis风格)
  56. // 示例:select * from user where name=#{name} → select * from user where name=?
  57. String sql = parseSqlTemplate(sqlTemplate);
  58. PreparedStatement ps = conn.prepareStatement(sql);
  59. // 步骤2:按参数顺序设置值(预编译核心,防止SQL注入)
  60. List<String> paramNames = SqlParamParser.extractParamNames(sqlTemplate);
  61. for (int i = 0; i < paramNames.size(); i++) {
  62. String paramName = paramNames.get(i);
  63. Object value = params.get(paramName);
  64. ps.setObject(i + 1, value); // JDBC参数索引从1开始
  65. }
  66. return ps;
  67. }
  68. /**
  69. * 解析SQL模板:将#{param}替换为?
  70. */
  71. private String parseSqlTemplate(String sqlTemplate) {
  72. return sqlTemplate.replaceAll("#\\{[^}]+\\}", "?");
  73. }
  74. }
  75. // 辅助类:提取SQL模板中的参数名
  76. class SqlParamParser {
  77. /**
  78. * 提取#{param}中的param名称,返回有序列表
  79. */
  80. public static List<String> extractParamNames(String sqlTemplate) {
  81. List<String> paramNames = new ArrayList<>();
  82. Pattern pattern = Pattern.compile("#\\{([^}]+)\\}");
  83. Matcher matcher = pattern.matcher(sqlTemplate);
  84. while (matcher.find()) {
  85. paramNames.add(matcher.group(1).trim());
  86. }
  87. return paramNames;
  88. }
  89. }
  90. // 辅助类:解析ResultSet为Map列表
  91. class ResultSetUtil {
  92. public static List<Map<String, Object>> parseResultSet(ResultSet rs) throws SQLException {
  93. List<Map<String, Object>> result = new ArrayList<>();
  94. ResultSetMetaData metaData = rs.getMetaData();
  95. int columnCount = metaData.getColumnCount();
  96. while (rs.next()) {
  97. Map<String, Object> row = new HashMap<>();
  98. for (int i = 1; i <= columnCount; i++) {
  99. String columnName = metaData.getColumnName(i);
  100. Object value = rs.getObject(i);
  101. row.put(columnName, value);
  102. }
  103. result.add(row);
  104. }
  105. return result;
  106. }
  107. }

3.4 Excel批量导入/更新实现(配置化+预编译)

  1. import com.alibaba.excel.EasyExcel;
  2. import com.alibaba.excel.read.listener.PageReadListener;
  3. import com.alibaba.fastjson2.JSON;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.jdbc.core.JdbcTemplate;
  6. import org.springframework.stereotype.Service;
  7. import org.springframework.transaction.annotation.Transactional;
  8. import org.springframework.util.Assert;
  9. import javax.sql.DataSource;
  10. import java.sql.Connection;
  11. import java.sql.PreparedStatement;
  12. import java.sql.SQLException;
  13. import java.util.*;
  14. @Service
  15. public class ExcelImportService {
  16. @Autowired
  17. private JdbcTemplate jdbcTemplate;
  18. @Autowired
  19. private DataSource dataSource;
  20. // 表名白名单(核心:防止表名注入,可从配置文件/数据库加载)
  21. private static final Set<String> TABLE_WHITELIST = new HashSet<>(Arrays.asList("user", "order", "product"));
  22. /**
  23. * 批量导入Excel数据(配置化表名+字段)
  24. * @param importCode 导入编码
  25. * @param excelFilePath Excel文件路径
  26. */
  27. @Transactional(rollbackFor = Exception.class)
  28. public void batchImport(String importCode, String excelFilePath) {
  29. // 1. 获取导入配置
  30. Map<String, Object> config = jdbcTemplate.queryForMap(
  31. "SELECT target_table, column_mapping FROM excel_import_config WHERE import_code = ?",
  32. importCode
  33. );
  34. String targetTable = config.get("target_table").toString();
  35. String columnMappingStr = config.get("column_mapping").toString();
  36. Map<String, String> columnMapping = JSON.parseObject(columnMappingStr, Map.class);
  37. // 2. 表名安全校验(核心:防止表名注入)
  38. Assert.isTrue(TABLE_WHITELIST.contains(targetTable), "不允许导入的表名:" + targetTable);
  39. // 3. 解析Excel并批量插入
  40. List<Map<String, Object>> dataList = new ArrayList<>();
  41. EasyExcel.read(excelFilePath, new PageReadListener<Map<Integer, String>>((data, context) -> {
  42. // 解析单行Excel数据(key为列索引,value为单元格值)
  43. Map<String, Object> rowData = parseExcelRow(data, columnMapping);
  44. dataList.add(rowData);
  45. // 批量提交(每1000条)
  46. if (dataList.size() >= 1000) {
  47. batchInsert(targetTable, columnMapping, dataList);
  48. dataList.clear();
  49. }
  50. })).sheet().doRead();
  51. // 处理剩余数据
  52. if (!dataList.isEmpty()) {
  53. batchInsert(targetTable, columnMapping, dataList);
  54. }
  55. }
  56. /**
  57. * 批量插入(使用预编译,防止参数注入)
  58. */
  59. private void batchInsert(String targetTable, Map<String, String> columnMapping, List<Map<String, Object>> dataList) {
  60. // 构建插入SQL:INSERT INTO user (name, age, phone) VALUES (?, ?, ?)
  61. List<String> dbColumns = new ArrayList<>(columnMapping.values());
  62. String columns = String.join(",", dbColumns);
  63. String placeholders = String.join(",", Collections.nCopies(dbColumns.size(), "?"));
  64. String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", targetTable, columns, placeholders);
  65. try (Connection conn = dataSource.getConnection()) {
  66. conn.setAutoCommit(false); // 关闭自动提交
  67. PreparedStatement ps = conn.prepareStatement(sql);
  68. // 批量设置参数(预编译核心)
  69. for (Map<String, Object> row : dataList) {
  70. for (int i = 0; i < dbColumns.size(); i++) {
  71. String column = dbColumns.get(i);
  72. Object value = row.get(column);
  73. ps.setObject(i + 1, value);
  74. }
  75. ps.addBatch(); // 添加到批处理
  76. }
  77. // 执行批处理
  78. ps.executeBatch();
  79. conn.commit(); // 提交事务
  80. } catch (SQLException e) {
  81. throw new RuntimeException("批量插入失败", e);
  82. }
  83. }
  84. /**
  85. * 解析Excel行数据,映射为数据库字段值
  86. */
  87. private Map<String, Object> parseExcelRow(Map<Integer, String> excelRow, Map<String, String> columnMapping) {
  88. Map<String, Object> rowData = new HashMap<>();
  89. // 假设Excel表头行是第一行,这里简化处理(实际需先读取表头)
  90. excelRow.forEach((index, value) -> {
  91. // 根据列索引获取Excel列名(实际需先解析表头映射)
  92. String excelColumnName = getExcelColumnNameByIndex(index);
  93. String dbColumn = columnMapping.get(excelColumnName);
  94. if (dbColumn != null) {
  95. rowData.put(dbColumn, value);
  96. }
  97. });
  98. return rowData;
  99. }
  100. /**
  101. * 模拟:根据列索引获取Excel列名(实际需从Excel表头解析)
  102. */
  103. private String getExcelColumnNameByIndex(Integer index) {
  104. Map<Integer, String> indexToName = new HashMap<>();
  105. indexToName.put(0, "姓名");
  106. indexToName.put(1, "年龄");
  107. indexToName.put(2, "手机号");
  108. return indexToName.get(index);
  109. }
  110. /**
  111. * 批量更新扩展(核心思路同批量插入)
  112. */
  113. private void batchUpdate(String targetTable, Map<String, String> columnMapping, List<Map<String, Object>> dataList) {
  114. // 假设主键为id,构建更新SQL:UPDATE user SET name=?, age=? WHERE id=?
  115. List<String> dbColumns = new ArrayList<>(columnMapping.values());
  116. dbColumns.remove("id"); // 移除主键列
  117. String updateSet = dbColumns.stream().map(col -> col + "=?").collect(Collectors.joining(","));
  118. String sql = String.format("UPDATE %s SET %s WHERE id=?", targetTable, updateSet);
  119. // 后续逻辑同批量插入,仅参数设置顺序不同(先设置更新列,再设置主键)
  120. try (Connection conn = dataSource.getConnection()) {
  121. conn.setAutoCommit(false);
  122. PreparedStatement ps = conn.prepareStatement(sql);
  123. for (Map<String, Object> row : dataList) {
  124. int paramIndex = 1;
  125. // 设置更新列参数
  126. for (String column : dbColumns) {
  127. ps.setObject(paramIndex++, row.get(column));
  128. }
  129. // 设置主键参数(WHERE条件)
  130. ps.setObject(paramIndex, row.get("id"));
  131. ps.addBatch();
  132. }
  133. ps.executeBatch();
  134. conn.commit();
  135. } catch (SQLException e) {
  136. throw new RuntimeException("批量更新失败", e);
  137. }
  138. }
  139. }

3.5 控制器层(测试接口)

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.web.bind.annotation.*;
  3. import org.springframework.web.multipart.MultipartFile;
  4. import java.io.File;
  5. import java.util.List;
  6. import java.util.Map;
  7. @RestController
  8. @RequestMapping("/config")
  9. public class ConfigController {
  10. @Autowired
  11. private ReportService reportService;
  12. @Autowired
  13. private ExcelImportService excelImportService;
  14. /**
  15. * 报表查询接口
  16. */
  17. @PostMapping("/report/{reportCode}")
  18. public List<Map<String, Object>> queryReport(
  19. @PathVariable String reportCode,
  20. @RequestBody Map<String, Object> params) {
  21. return reportService.executeReport(reportCode, params);
  22. }
  23. /**
  24. * Excel导入接口
  25. */
  26. @PostMapping("/import/{importCode}")
  27. public String importExcel(
  28. @PathVariable String importCode,
  29. @RequestParam("file") MultipartFile file) {
  30. // 保存文件到临时目录(实际项目建议使用流式处理,避免落地)
  31. File tempFile = new File(System.getProperty("java.io.tmpdir") + "/" + file.getOriginalFilename());
  32. try {
  33. file.transferTo(tempFile);
  34. excelImportService.batchImport(importCode, tempFile.getAbsolutePath());
  35. return "导入成功";
  36. } catch (Exception e) {
  37. return "导入失败:" + e.getMessage();
  38. }
  39. }
  40. }

四、关键安全保障说明

  1. 参数预编译:所有前端传递的参数均通过PreparedStatement.setObject()设置,MyBatis/JDBC自动做参数转义,彻底防止SQL注入

  2. 表名/字段名白名单:对配置的表名做白名单校验,仅允许导入到指定表,避免恶意配置user; drop table xxx等危险表名

  3. SQL模板校验:可扩展添加SQL模板语法校验,禁止配置包含;、DROP、ALTER等危险关键字的SQL

  4. 权限控制:建议为报表配置/Excel导入配置添加权限校验,仅允许管理员配置SQL和导入规则

五、总结

  1. 预编译核心:将配置的SQL模板中的#{参数}替换为?,通过PreparedStatement设置参数,保留JDBC预编译能力,杜绝参数注入

  2. 配置化安全:表名/字段名通过白名单校验,仅允许合法配置,防止表名注入

  3. 批量操作优化:使用JDBC批处理(addBatch()+executeBatch())提升Excel导入/更新效率,结合事务保证数据一致性

该方案完全利用MyBatis/JDBC的预编译特性,同时满足报表SQL和Excel导入的全配置化需求,兼顾安全性和灵活性,可直接集成到SpringBoot项目中使用。

(注:文档部分内容可能由 AI 生成)

 9

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2