一、需求核心
可配置报表系统:报表查询 SQL 存储在数据库,前端传递参数,需利用 MyBatis 预编译防止 SQL 注入。
批量导入/更新:Excel 导入目标表名、字段映射可配置,同样保证预编译特性与安全性,贴合 MyBatis Mapper+XML 主流开发模式。
二、核心实现思路
复用 MyBatis 生态:基于 Mapper 接口、SqlSource 解析、预编译机制,不脱离日常开发模式。
SQL 模板设计:数据库中配置的 SQL 直接使用 MyBatis 风格
#{参数名}占位符,保障预编译能力。动态 SQL 执行:通过 MyBatis
@SelectProvider/@InsertProvider注解或 XML 动态 SQL 语法,解析数据库中的 SQL 模板。安全校验机制:表名、字段名通过白名单校验(防止注入),参数统一用
#{}包裹(预编译转义)。配置化映射:设计报表 SQL 配置表、Excel 导入配置表,存储 SQL 模板、表名、字段映射关系。
三、具体实现
3.1 数据库表设计(核心配置表)
-- 报表SQL配置表:存储可配置的报表查询模板CREATE TABLE report_config (id BIGINT PRIMARY KEY AUTO_INCREMENT,report_code VARCHAR(50) NOT NULL COMMENT '报表编码(唯一)',sql_template TEXT NOT NULL COMMENT 'SQL模板(使用#{参数名}占位符,如:SELECT * FROM user WHERE name = #{name})',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_report_code (report_code)) COMMENT '报表SQL配置表';-- Excel导入配置表:存储导入规则CREATE TABLE excel_import_config (id BIGINT PRIMARY KEY AUTO_INCREMENT,import_code VARCHAR(50) NOT NULL COMMENT '导入编码(唯一)',target_table VARCHAR(50) NOT NULL COMMENT '目标表名',column_mapping JSON NOT NULL COMMENT '列映射:{"excel列名":"数据库字段名"}',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_import_code (import_code)) COMMENT 'Excel导入配置表';-- 初始化测试数据INSERT INTO report_config (report_code, sql_template) VALUES('user_query', 'SELECT * FROM user WHERE name = #{name} AND age = #{age}');INSERT INTO excel_import_config (import_code, target_table, column_mapping) VALUES('user_import', 'user', '{"姓名":"name","年龄":"age","手机号":"phone"}');
3.2 核心依赖(pom.xml)
<dependencies><!-- SpringBoot核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis整合SpringBoot --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version></dependency><!-- MySQL驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- EasyExcel:Excel解析 --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</version></dependency><!-- 工具类 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.22</version></dependency></dependencies>
3.3 核心通用 Mapper 设计(关键)
定义通用 Mapper 接口,统一处理动态 SQL 执行、批量插入,贴合 MyBatis 开发规范。
import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.InsertProvider;import org.apache.ibatis.annotations.SelectProvider;import java.util.List;import java.util.Map;/*** 通用动态SQL执行Mapper(支持可配置报表查询、批量导入)*/public interface DynamicSqlMapper {/*** 执行动态查询SQL(报表查询)* @param sqlTemplate 从数据库加载的SQL模板(含#{参数})* @param params 前端传递的参数* @return 查询结果(Map列表,key为字段名)*/@SelectProvider(type = DynamicSqlProvider.class, method = "buildQuerySql")List<Map<String, Object>> executeDynamicQuery(@Param("sqlTemplate") String sqlTemplate,@Param("params") Map<String, Object> params);/*** 批量插入(Excel导入核心方法)* @param tableName 目标表名(已做白名单校验)* @param columns 数据库字段列表* @param dataList 导入数据列表(Mapkey为字段名,value为值)* @return 影响行数*/@InsertProvider(type = DynamicSqlProvider.class, method = "buildBatchInsertSql")int batchInsert(@Param("tableName") String tableName,@Param("columns") List<String> columns,@Param("dataList") List<Map<String, Object>> dataList);}
3.4 SQL 构建器(Provider):复用 MyBatis 预编译
通过 Provider 动态构建 SQL,参数仍用 #{} 包裹,保障预编译能力。
import org.apache.ibatis.jdbc.SQL;import java.util.List;import java.util.Map;/*** SQL构建器:动态拼接SQL,兼容MyBatis预编译*/public class DynamicSqlProvider {/*** 构建动态查询SQL(直接返回数据库中的模板,MyBatis自动解析预编译)*/public String buildQuerySql(Map<String, Object> paramMap) {// paramMap包含@Param传递的sqlTemplate和paramsString sqlTemplate = (String) paramMap.get("sqlTemplate");return sqlTemplate; // MyBatis会自动解析#{params.xxx}并预编译}/*** 构建批量插入SQL(表名拼接+参数预编译)*/public String buildBatchInsertSql(Map<String, Object> paramMap) {String tableName = (String) paramMap.get("tableName");List<String> columns = (List<String>) paramMap.get("columns");List<Map<String, Object>> dataList = (List<Map<String, Object>>) paramMap.get("dataList");// 1. 拼接插入SQL框架(表名已校验,字段名固定配置)SQL sql = new SQL().INSERT_INTO(tableName);columns.forEach(sql::INTO_COLUMNS); // 添加字段列表// 2. 批量添加值:每个值用#{dataList[索引].字段名}保证预编译for (int i = 0; i < dataList.size(); i++) {StringBuilder values = new StringBuilder();for (int j = 0; j < columns.size(); j++) {String column = columns.get(j);values.append("#{dataList[").append(i).append("].").append(column).append("}");if (j < columns.size() - 1) {values.append(",");}}sql.VALUES(String.join(",", columns), values.toString());}return sql.toString();}}
3.5 报表查询服务实现
从数据库加载 SQL 模板,调用通用 Mapper 执行,利用 MyBatis 预编译处理参数。
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Service;import org.springframework.util.Assert;import java.util.List;import java.util.Map;@Servicepublic class ReportService {@Autowiredprivate JdbcTemplate jdbcTemplate; // 用于查询SQL模板@Autowiredprivate DynamicSqlMapper dynamicSqlMapper; // 通用动态SQL Mapper/*** 执行配置化报表查询* @param reportCode 报表编码(关联report_config表)* @param params 前端传递的查询参数* @return 报表结果*/public List<Map<String, Object>> executeReport(String reportCode, Map<String, Object> params) {// 1. 从数据库查询SQL模板String sqlTemplate = jdbcTemplate.queryForObject("SELECT sql_template FROM report_config WHERE report_code = ?",new Object[]{reportCode},String.class);Assert.notNull(sqlTemplate, "报表配置不存在:" + reportCode);// 2. 调用通用Mapper执行:MyBatis自动解析#{参数}并预编译return dynamicSqlMapper.executeDynamicQuery(sqlTemplate, params);}}
3.6 Excel 批量导入服务实现
加载导入配置,解析 Excel 数据,调用通用 Mapper 批量插入,通过白名单校验表名。
import com.alibaba.excel.EasyExcel;import com.alibaba.excel.read.listener.PageReadListener;import com.alibaba.fastjson2.JSON;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.util.Assert;import java.util.*;@Servicepublic class ExcelImportService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate DynamicSqlMapper dynamicSqlMapper;// 表名白名单:仅允许导入指定表,防止恶意注入(可从配置文件/数据库加载)private static final Set<String> TABLE_WHITELIST = new HashSet<>(Arrays.asList("user", "order", "product"));/*** 批量导入Excel数据* @param importCode 导入编码(关联excel_import_config表)* @param excelFilePath Excel文件路径*/@Transactional(rollbackFor = Exception.class) // 事务保证数据一致性public void batchImport(String importCode, String excelFilePath) {// 1. 从数据库获取导入配置Map<String, Object> config = jdbcTemplate.queryForMap("SELECT target_table, column_mapping FROM excel_import_config WHERE import_code = ?",importCode);String targetTable = config.get("target_table").toString();String columnMappingStr = config.get("column_mapping").toString();Map<String, String> columnMapping = JSON.parseObject(columnMappingStr, Map.class); // Excel列→数据库字段映射// 2. 表名安全校验(核心:防止表名注入)Assert.isTrue(TABLE_WHITELIST.contains(targetTable), "不允许导入的表名:" + targetTable);List<String> dbColumns = new ArrayList<>(columnMapping.values()); // 数据库字段列表// 3. EasyExcel解析Excel数据(流式读取,避免内存溢出)List<Map<String, Object>> dataList = new ArrayList<>();EasyExcel.read(excelFilePath, new PageReadListener<Map<Integer, String>>((data, context) -> {// 解析单行数据:映射为数据库字段-值Map<String, Object> rowData = parseExcelRow(data, columnMapping);dataList.add(rowData);// 每1000条批量提交(优化性能)if (dataList.size() >= 1000) {dynamicSqlMapper.batchInsert(targetTable, dbColumns, dataList);dataList.clear();}})).sheet().doRead();// 处理剩余不足1000条的数据if (!dataList.isEmpty()) {dynamicSqlMapper.batchInsert(targetTable, dbColumns, dataList);}}/*** 解析Excel行数据:将Excel列索引→值映射为数据库字段→值*/private Map<String, Object> parseExcelRow(Map<Integer, String> excelRow, Map<String, String> columnMapping) {Map<String, Object> rowData = new HashMap<>();// 模拟Excel列索引→列名映射(实际应先读取Excel表头行)Map<Integer, String> indexToExcelName = new HashMap<>();indexToExcelName.put(0, "姓名");indexToExcelName.put(1, "年龄");indexToExcelName.put(2, "手机号");// 匹配Excel列名与数据库字段excelRow.forEach((index, value) -> {String excelColumnName = indexToExcelName.get(index);String dbColumn = columnMapping.get(excelColumnName);if (dbColumn != null) {rowData.put(dbColumn, value);}});return rowData;}}
3.7 控制器层(测试接口)
提供 HTTP 接口,接收前端参数/Excel 文件,调用服务层执行。
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import java.io.File;import java.util.List;import java.util.Map;@RestController@RequestMapping("/config")public class ConfigController {@Autowiredprivate ReportService reportService;@Autowiredprivate ExcelImportService excelImportService;/*** 报表查询接口* @param reportCode 报表编码* @param params 前端传递的查询参数(JSON格式)* @return 报表结果*/@PostMapping("/report/{reportCode}")public List<Map<String, Object>> queryReport(@PathVariable String reportCode,@RequestBody Map<String, Object> params) {return reportService.executeReport(reportCode, params);}/*** Excel批量导入接口* @param importCode 导入编码* @param file 上传的Excel文件* @return 导入结果*/@PostMapping("/import/{importCode}")public String importExcel(@PathVariable String importCode,@RequestParam("file") MultipartFile file) {// 临时保存文件(实际项目建议流式处理,避免文件落地)File tempFile = new File(System.getProperty("java.io.tmpdir") + "/" + file.getOriginalFilename());try {file.transferTo(tempFile);excelImportService.batchImport(importCode, tempFile.getAbsolutePath());return "导入成功";} catch (Exception e) {return "导入失败:" + e.getMessage();}}}
3.8 MyBatis 配置(Mapper 扫描)
配置 Mapper 接口扫描路径,确保 MyBatis 能识别 Mapper。
import org.mybatis.spring.annotation.MapperScan;import org.springframework.context.annotation.Configuration;/*** MyBatis核心配置*/@Configuration@MapperScan("com.yourpackage.mapper") // 替换为你的Mapper接口实际包路径public class MyBatisConfig {}
四、XML 方式实现动态 SQL(可选,兼容偏好)
若更习惯 XML 编写 SQL,可将 Provider 逻辑迁移到 XML 中,核心逻辑一致(静态拼接+参数预编译)。
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.yourpackage.mapper.DynamicSqlMapper"><!-- 动态查询(XML版):sqlTemplate由数据库提供,参数自动预编译 --><!-- 批量插入(XML版):表名/字段静态拼接,参数预编译 --><insert id="batchInsert">INSERT INTO ${tableName} (<foreach collection="columns" item="column" separator=",">${column} <!-- 字段名已通过配置校验,安全 --></foreach>) VALUES<foreach collection="dataList" item="item" separator=",">(<foreach collection="columns" item="column" separator=",">#{item.${column}} <!-- 核心:参数预编译,防止注入 --></foreach>)</foreach></insert></mapper>
五、批量更新扩展实现
批量更新逻辑与批量插入类似,核心是构建动态更新 SQL,参数预编译。
// 1. 在DynamicSqlMapper中添加批量更新方法@InsertProvider(type = DynamicSqlProvider.class, method = "buildBatchUpdateSql")int batchUpdate(@Param("tableName") String tableName,@Param("columns") List<String> columns,@Param("dataList") List<Map<String, Object>> dataList);// 2. 在DynamicSqlProvider中添加构建方法public String buildBatchUpdateSql(Map<String, Object> paramMap) {String tableName = (String) paramMap.get("tableName");List<String> columns = (List<String>) paramMap.get("columns");List<Map<String, Object>> dataList = (List<Map<String, Object>>) paramMap.get("dataList");// 假设主键为id,移除主键字段(更新无需主键字段)columns.remove("id");// 构建更新SQL框架:UPDATE 表 SET 字段1=?,字段2=? WHERE id=?SQL sql = new SQL().UPDATE(tableName);columns.forEach(column -> sql.SET(column + "=#{item." + column + "}"));sql.WHERE("id=#{item.id}");// 批量拼接更新语句(MyBatis支持多条UPDATE语句批量执行)StringBuilder batchSql = new StringBuilder();for (int i = 0; i < dataList.size(); i++) {batchSql.append(sql.toString().replace("item.", "dataList[" + i + "]."));if (i < dataList.size() - 1) {batchSql.append(";");}}return batchSql.toString();}
六、关键安全保障说明
参数预编译:所有前端传递的参数均通过 MyBatis
#{}处理,自动转义特殊字符(如单引号、分号),彻底杜绝参数注入。表名/字段名白名单:导入的目标表名必须在白名单内,防止恶意配置
user; DROP TABLE xxx等危险表名。SQL 模板校验:可扩展添加模板校验逻辑,禁止配置包含
DROP、ALTER、TRUNCATE等危险关键字的 SQL 模板。权限控制:报表配置、导入配置仅开放给管理员,避免普通用户修改配置引入安全风险。
七、核心总结
Mapper 模式完全兼容:通过
@Provider注解或 XML 动态 SQL,可在保留 Mapper 体系的前提下实现全配置化。预编译不失效:无论 SQL 来源是 XML 还是数据库,只要参数用
#{}包裹,MyBatis 就会自动触发预编译。安全与灵活平衡:静态配置项(表名、字段名)靠白名单校验,动态参数靠预编译,两者结合保障系统安全。
性能优化:批量操作使用 MyBatis+JDBC 批处理机制(
addBatch()+executeBatch()),配合事务提升效率与数据一致性。(注:文档部分内容可能由 AI 生成)
