个人随笔
目录
SpringBoot+MyBatis 实现可配置报表与批量导入更新笔记(mybatis模式)
2026-01-14 11:01:42

一、需求核心

  1. 可配置报表系统:报表查询 SQL 存储在数据库,前端传递参数,需利用 MyBatis 预编译防止 SQL 注入。

  2. 批量导入/更新:Excel 导入目标表名、字段映射可配置,同样保证预编译特性与安全性,贴合 MyBatis Mapper+XML 主流开发模式。

二、核心实现思路

  1. 复用 MyBatis 生态:基于 Mapper 接口、SqlSource 解析、预编译机制,不脱离日常开发模式。

  2. SQL 模板设计:数据库中配置的 SQL 直接使用 MyBatis 风格 #{参数名} 占位符,保障预编译能力。

  3. 动态 SQL 执行:通过 MyBatis@SelectProvider/@InsertProvider 注解或 XML 动态 SQL 语法,解析数据库中的 SQL 模板。

  4. 安全校验机制:表名、字段名通过白名单校验(防止注入),参数统一用 #{} 包裹(预编译转义)。

  5. 配置化映射:设计报表 SQL 配置表、Excel 导入配置表,存储 SQL 模板、表名、字段映射关系。

三、具体实现

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模板(使用#{参数名}占位符,如:SELECT * FROM user WHERE name = #{name})',
  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</artifactId>
  6. </dependency>
  7. <!-- MyBatis整合SpringBoot -->
  8. <dependency>
  9. <groupId>org.mybatis.spring.boot</groupId>
  10. <artifactId>mybatis-spring-boot-starter</artifactId>
  11. <version>3.0.3</version>
  12. </dependency>
  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>
  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 核心通用 Mapper 设计(关键)

定义通用 Mapper 接口,统一处理动态 SQL 执行、批量插入,贴合 MyBatis 开发规范。

  1. import org.apache.ibatis.annotations.Param;
  2. import org.apache.ibatis.annotations.InsertProvider;
  3. import org.apache.ibatis.annotations.SelectProvider;
  4. import java.util.List;
  5. import java.util.Map;
  6. /**
  7. * 通用动态SQL执行Mapper(支持可配置报表查询、批量导入)
  8. */
  9. public interface DynamicSqlMapper {
  10. /**
  11. * 执行动态查询SQL(报表查询)
  12. * @param sqlTemplate 从数据库加载的SQL模板(含#{参数})
  13. * @param params 前端传递的参数
  14. * @return 查询结果(Map列表,key为字段名)
  15. */
  16. @SelectProvider(type = DynamicSqlProvider.class, method = "buildQuerySql")
  17. List<Map<String, Object>> executeDynamicQuery(
  18. @Param("sqlTemplate") String sqlTemplate,
  19. @Param("params") Map<String, Object> params);
  20. /**
  21. * 批量插入(Excel导入核心方法)
  22. * @param tableName 目标表名(已做白名单校验)
  23. * @param columns 数据库字段列表
  24. * @param dataList 导入数据列表(Mapkey为字段名,value为值)
  25. * @return 影响行数
  26. */
  27. @InsertProvider(type = DynamicSqlProvider.class, method = "buildBatchInsertSql")
  28. int batchInsert(
  29. @Param("tableName") String tableName,
  30. @Param("columns") List<String> columns,
  31. @Param("dataList") List<Map<String, Object>> dataList);
  32. }

3.4 SQL 构建器(Provider):复用 MyBatis 预编译

通过 Provider 动态构建 SQL,参数仍用 #{} 包裹,保障预编译能力。

  1. import org.apache.ibatis.jdbc.SQL;
  2. import java.util.List;
  3. import java.util.Map;
  4. /**
  5. * SQL构建器:动态拼接SQL,兼容MyBatis预编译
  6. */
  7. public class DynamicSqlProvider {
  8. /**
  9. * 构建动态查询SQL(直接返回数据库中的模板,MyBatis自动解析预编译)
  10. */
  11. public String buildQuerySql(Map<String, Object> paramMap) {
  12. // paramMap包含@Param传递的sqlTemplate和params
  13. String sqlTemplate = (String) paramMap.get("sqlTemplate");
  14. return sqlTemplate; // MyBatis会自动解析#{params.xxx}并预编译
  15. }
  16. /**
  17. * 构建批量插入SQL(表名拼接+参数预编译)
  18. */
  19. public String buildBatchInsertSql(Map<String, Object> paramMap) {
  20. String tableName = (String) paramMap.get("tableName");
  21. List<String> columns = (List<String>) paramMap.get("columns");
  22. List<Map<String, Object>> dataList = (List<Map<String, Object>>) paramMap.get("dataList");
  23. // 1. 拼接插入SQL框架(表名已校验,字段名固定配置)
  24. SQL sql = new SQL().INSERT_INTO(tableName);
  25. columns.forEach(sql::INTO_COLUMNS); // 添加字段列表
  26. // 2. 批量添加值:每个值用#{dataList[索引].字段名}保证预编译
  27. for (int i = 0; i < dataList.size(); i++) {
  28. StringBuilder values = new StringBuilder();
  29. for (int j = 0; j < columns.size(); j++) {
  30. String column = columns.get(j);
  31. values.append("#{dataList[").append(i).append("].").append(column).append("}");
  32. if (j < columns.size() - 1) {
  33. values.append(",");
  34. }
  35. }
  36. sql.VALUES(String.join(",", columns), values.toString());
  37. }
  38. return sql.toString();
  39. }
  40. }

3.5 报表查询服务实现

从数据库加载 SQL 模板,调用通用 Mapper 执行,利用 MyBatis 预编译处理参数。

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.jdbc.core.JdbcTemplate;
  3. import org.springframework.stereotype.Service;
  4. import org.springframework.util.Assert;
  5. import java.util.List;
  6. import java.util.Map;
  7. @Service
  8. public class ReportService {
  9. @Autowired
  10. private JdbcTemplate jdbcTemplate; // 用于查询SQL模板
  11. @Autowired
  12. private DynamicSqlMapper dynamicSqlMapper; // 通用动态SQL Mapper
  13. /**
  14. * 执行配置化报表查询
  15. * @param reportCode 报表编码(关联report_config表)
  16. * @param params 前端传递的查询参数
  17. * @return 报表结果
  18. */
  19. public List<Map<String, Object>> executeReport(String reportCode, Map<String, Object> params) {
  20. // 1. 从数据库查询SQL模板
  21. String sqlTemplate = jdbcTemplate.queryForObject(
  22. "SELECT sql_template FROM report_config WHERE report_code = ?",
  23. new Object[]{reportCode},
  24. String.class
  25. );
  26. Assert.notNull(sqlTemplate, "报表配置不存在:" + reportCode);
  27. // 2. 调用通用Mapper执行:MyBatis自动解析#{参数}并预编译
  28. return dynamicSqlMapper.executeDynamicQuery(sqlTemplate, params);
  29. }
  30. }

3.6 Excel 批量导入服务实现

加载导入配置,解析 Excel 数据,调用通用 Mapper 批量插入,通过白名单校验表名。

  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 java.util.*;
  10. @Service
  11. public class ExcelImportService {
  12. @Autowired
  13. private JdbcTemplate jdbcTemplate;
  14. @Autowired
  15. private DynamicSqlMapper dynamicSqlMapper;
  16. // 表名白名单:仅允许导入指定表,防止恶意注入(可从配置文件/数据库加载)
  17. private static final Set<String> TABLE_WHITELIST = new HashSet<>(Arrays.asList("user", "order", "product"));
  18. /**
  19. * 批量导入Excel数据
  20. * @param importCode 导入编码(关联excel_import_config表)
  21. * @param excelFilePath Excel文件路径
  22. */
  23. @Transactional(rollbackFor = Exception.class) // 事务保证数据一致性
  24. public void batchImport(String importCode, String excelFilePath) {
  25. // 1. 从数据库获取导入配置
  26. Map<String, Object> config = jdbcTemplate.queryForMap(
  27. "SELECT target_table, column_mapping FROM excel_import_config WHERE import_code = ?",
  28. importCode
  29. );
  30. String targetTable = config.get("target_table").toString();
  31. String columnMappingStr = config.get("column_mapping").toString();
  32. Map<String, String> columnMapping = JSON.parseObject(columnMappingStr, Map.class); // Excel列→数据库字段映射
  33. // 2. 表名安全校验(核心:防止表名注入)
  34. Assert.isTrue(TABLE_WHITELIST.contains(targetTable), "不允许导入的表名:" + targetTable);
  35. List<String> dbColumns = new ArrayList<>(columnMapping.values()); // 数据库字段列表
  36. // 3. EasyExcel解析Excel数据(流式读取,避免内存溢出)
  37. List<Map<String, Object>> dataList = new ArrayList<>();
  38. EasyExcel.read(excelFilePath, new PageReadListener<Map<Integer, String>>((data, context) -> {
  39. // 解析单行数据:映射为数据库字段-值
  40. Map<String, Object> rowData = parseExcelRow(data, columnMapping);
  41. dataList.add(rowData);
  42. // 每1000条批量提交(优化性能)
  43. if (dataList.size() >= 1000) {
  44. dynamicSqlMapper.batchInsert(targetTable, dbColumns, dataList);
  45. dataList.clear();
  46. }
  47. })).sheet().doRead();
  48. // 处理剩余不足1000条的数据
  49. if (!dataList.isEmpty()) {
  50. dynamicSqlMapper.batchInsert(targetTable, dbColumns, dataList);
  51. }
  52. }
  53. /**
  54. * 解析Excel行数据:将Excel列索引→值映射为数据库字段→值
  55. */
  56. private Map<String, Object> parseExcelRow(Map<Integer, String> excelRow, Map<String, String> columnMapping) {
  57. Map<String, Object> rowData = new HashMap<>();
  58. // 模拟Excel列索引→列名映射(实际应先读取Excel表头行)
  59. Map<Integer, String> indexToExcelName = new HashMap<>();
  60. indexToExcelName.put(0, "姓名");
  61. indexToExcelName.put(1, "年龄");
  62. indexToExcelName.put(2, "手机号");
  63. // 匹配Excel列名与数据库字段
  64. excelRow.forEach((index, value) -> {
  65. String excelColumnName = indexToExcelName.get(index);
  66. String dbColumn = columnMapping.get(excelColumnName);
  67. if (dbColumn != null) {
  68. rowData.put(dbColumn, value);
  69. }
  70. });
  71. return rowData;
  72. }
  73. }

3.7 控制器层(测试接口)

提供 HTTP 接口,接收前端参数/Excel 文件,调用服务层执行。

  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. * @param reportCode 报表编码
  17. * @param params 前端传递的查询参数(JSON格式)
  18. * @return 报表结果
  19. */
  20. @PostMapping("/report/{reportCode}")
  21. public List<Map<String, Object>> queryReport(
  22. @PathVariable String reportCode,
  23. @RequestBody Map<String, Object> params) {
  24. return reportService.executeReport(reportCode, params);
  25. }
  26. /**
  27. * Excel批量导入接口
  28. * @param importCode 导入编码
  29. * @param file 上传的Excel文件
  30. * @return 导入结果
  31. */
  32. @PostMapping("/import/{importCode}")
  33. public String importExcel(
  34. @PathVariable String importCode,
  35. @RequestParam("file") MultipartFile file) {
  36. // 临时保存文件(实际项目建议流式处理,避免文件落地)
  37. File tempFile = new File(System.getProperty("java.io.tmpdir") + "/" + file.getOriginalFilename());
  38. try {
  39. file.transferTo(tempFile);
  40. excelImportService.batchImport(importCode, tempFile.getAbsolutePath());
  41. return "导入成功";
  42. } catch (Exception e) {
  43. return "导入失败:" + e.getMessage();
  44. }
  45. }
  46. }

3.8 MyBatis 配置(Mapper 扫描)

配置 Mapper 接口扫描路径,确保 MyBatis 能识别 Mapper。

  1. import org.mybatis.spring.annotation.MapperScan;
  2. import org.springframework.context.annotation.Configuration;
  3. /**
  4. * MyBatis核心配置
  5. */
  6. @Configuration
  7. @MapperScan("com.yourpackage.mapper") // 替换为你的Mapper接口实际包路径
  8. public class MyBatisConfig {
  9. }

四、XML 方式实现动态 SQL(可选,兼容偏好)

若更习惯 XML 编写 SQL,可将 Provider 逻辑迁移到 XML 中,核心逻辑一致(静态拼接+参数预编译)。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.yourpackage.mapper.DynamicSqlMapper">
  3. <!-- 动态查询(XML版):sqlTemplate由数据库提供,参数自动预编译 -->
  4. <!-- 批量插入(XML版):表名/字段静态拼接,参数预编译 -->
  5. <insert id="batchInsert">
  6. INSERT INTO ${tableName} (
  7. <foreach collection="columns" item="column" separator=",">
  8. ${column} <!-- 字段名已通过配置校验,安全 -->
  9. </foreach>
  10. ) VALUES<foreach collection="dataList" item="item" separator=",">
  11. (
  12. <foreach collection="columns" item="column" separator=",">
  13. #{item.${column}} <!-- 核心:参数预编译,防止注入 -->
  14. </foreach>
  15. )
  16. </foreach>
  17. </insert>
  18. </mapper>

五、批量更新扩展实现

批量更新逻辑与批量插入类似,核心是构建动态更新 SQL,参数预编译。

  1. // 1. 在DynamicSqlMapper中添加批量更新方法
  2. @InsertProvider(type = DynamicSqlProvider.class, method = "buildBatchUpdateSql")
  3. int batchUpdate(
  4. @Param("tableName") String tableName,
  5. @Param("columns") List<String> columns,
  6. @Param("dataList") List<Map<String, Object>> dataList);
  7. // 2. 在DynamicSqlProvider中添加构建方法
  8. public String buildBatchUpdateSql(Map<String, Object> paramMap) {
  9. String tableName = (String) paramMap.get("tableName");
  10. List<String> columns = (List<String>) paramMap.get("columns");
  11. List<Map<String, Object>> dataList = (List<Map<String, Object>>) paramMap.get("dataList");
  12. // 假设主键为id,移除主键字段(更新无需主键字段)
  13. columns.remove("id");
  14. // 构建更新SQL框架:UPDATE 表 SET 字段1=?,字段2=? WHERE id=?
  15. SQL sql = new SQL().UPDATE(tableName);
  16. columns.forEach(column -> sql.SET(column + "=#{item." + column + "}"));
  17. sql.WHERE("id=#{item.id}");
  18. // 批量拼接更新语句(MyBatis支持多条UPDATE语句批量执行)
  19. StringBuilder batchSql = new StringBuilder();
  20. for (int i = 0; i < dataList.size(); i++) {
  21. batchSql.append(sql.toString().replace("item.", "dataList[" + i + "]."));
  22. if (i < dataList.size() - 1) {
  23. batchSql.append(";");
  24. }
  25. }
  26. return batchSql.toString();
  27. }

六、关键安全保障说明

  1. 参数预编译:所有前端传递的参数均通过 MyBatis #{} 处理,自动转义特殊字符(如单引号、分号),彻底杜绝参数注入。

  2. 表名/字段名白名单:导入的目标表名必须在白名单内,防止恶意配置 user; DROP TABLE xxx 等危险表名。

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

  4. 权限控制:报表配置、导入配置仅开放给管理员,避免普通用户修改配置引入安全风险。

七、核心总结

  1. Mapper 模式完全兼容:通过 @Provider 注解或 XML 动态 SQL,可在保留 Mapper 体系的前提下实现全配置化。

  2. 预编译不失效:无论 SQL 来源是 XML 还是数据库,只要参数用 #{} 包裹,MyBatis 就会自动触发预编译。

  3. 安全与灵活平衡:静态配置项(表名、字段名)靠白名单校验,动态参数靠预编译,两者结合保障系统安全。

  4. 性能优化:批量操作使用 MyBatis+JDBC 批处理机制(addBatch()+executeBatch()),配合事务提升效率与数据一致性。

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

 8

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


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

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