一、需求概述
核心需求:
可配置报表系统:报表查询SQL配置在数据库中,前端传递参数,需利用MyBatis预编译功能防止SQL注入,避免安全问题
Excel批量导入/更新:导入目标表名、参数均为可配置,同时保证安全性和预编译特性
二、核心实现思路
核心原则:分离SQL模板静态部分与动态参数,MyBatis预编译处理参数,表名/字段名等配置项通过安全校验后拼接
SQL模板设计:配置在数据库的SQL使用MyBatis风格参数占位符(如#{name}),保留预编译能力
动态SQL执行:通过MyBatis的SqlSession手动构建PreparedStatement,利用预编译处理参数
表名/字段名安全校验:对配置的表名、字段名做白名单校验,防止恶意注入
Excel导入配置化:设计导入配置表,关联Excel列、目标表、字段映射关系,批量操作采用MyBatis批量语法
三、具体实现代码
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模板(使用#{参数名}占位符)',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 --><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 报表查询实现(利用MyBatis预编译)
import org.apache.ibatis.session.SqlSession;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Service;import org.springframework.util.Assert;import javax.sql.DataSource;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.regex.Matcher;import java.util.regex.Pattern;@Servicepublic class ReportService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate SqlSession sqlSession;@Autowiredprivate DataSource dataSource;/*** 执行配置化报表查询* @param reportCode 报表编码* @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. 利用MyBatis的预编译执行SQL(核心:保留预编译能力)try (Connection conn = dataSource.getConnection()) {// 构建预编译StatementPreparedStatement ps = createPreparedStatement(conn, sqlTemplate, params);// 执行查询ResultSet rs = ps.executeQuery();// 解析结果集为Map列表return ResultSetUtil.parseResultSet(rs);} catch (SQLException e) {throw new RuntimeException("执行报表SQL失败", e);}}/*** 创建预编译Statement(核心:参数预编译,防止注入)*/private PreparedStatement createPreparedStatement(Connection conn, String sqlTemplate, Map<String, Object> params) throws SQLException {// 步骤1:解析SQL模板中的参数名,替换为?(保持MyBatis风格)// 示例:select * from user where name=#{name} → select * from user where name=?String sql = parseSqlTemplate(sqlTemplate);PreparedStatement ps = conn.prepareStatement(sql);// 步骤2:按参数顺序设置值(预编译核心,防止SQL注入)List<String> paramNames = SqlParamParser.extractParamNames(sqlTemplate);for (int i = 0; i < paramNames.size(); i++) {String paramName = paramNames.get(i);Object value = params.get(paramName);ps.setObject(i + 1, value); // JDBC参数索引从1开始}return ps;}/*** 解析SQL模板:将#{param}替换为?*/private String parseSqlTemplate(String sqlTemplate) {return sqlTemplate.replaceAll("#\\{[^}]+\\}", "?");}}// 辅助类:提取SQL模板中的参数名class SqlParamParser {/*** 提取#{param}中的param名称,返回有序列表*/public static List<String> extractParamNames(String sqlTemplate) {List<String> paramNames = new ArrayList<>();Pattern pattern = Pattern.compile("#\\{([^}]+)\\}");Matcher matcher = pattern.matcher(sqlTemplate);while (matcher.find()) {paramNames.add(matcher.group(1).trim());}return paramNames;}}// 辅助类:解析ResultSet为Map列表class ResultSetUtil {public static List<Map<String, Object>> parseResultSet(ResultSet rs) throws SQLException {List<Map<String, Object>> result = new ArrayList<>();ResultSetMetaData metaData = rs.getMetaData();int columnCount = metaData.getColumnCount();while (rs.next()) {Map<String, Object> row = new HashMap<>();for (int i = 1; i <= columnCount; i++) {String columnName = metaData.getColumnName(i);Object value = rs.getObject(i);row.put(columnName, value);}result.add(row);}return result;}}
3.4 Excel批量导入/更新实现(配置化+预编译)
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 javax.sql.DataSource;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.SQLException;import java.util.*;@Servicepublic class ExcelImportService {@Autowiredprivate JdbcTemplate jdbcTemplate;@Autowiredprivate DataSource dataSource;// 表名白名单(核心:防止表名注入,可从配置文件/数据库加载)private static final Set<String> TABLE_WHITELIST = new HashSet<>(Arrays.asList("user", "order", "product"));/*** 批量导入Excel数据(配置化表名+字段)* @param importCode 导入编码* @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);// 2. 表名安全校验(核心:防止表名注入)Assert.isTrue(TABLE_WHITELIST.contains(targetTable), "不允许导入的表名:" + targetTable);// 3. 解析Excel并批量插入List<Map<String, Object>> dataList = new ArrayList<>();EasyExcel.read(excelFilePath, new PageReadListener<Map<Integer, String>>((data, context) -> {// 解析单行Excel数据(key为列索引,value为单元格值)Map<String, Object> rowData = parseExcelRow(data, columnMapping);dataList.add(rowData);// 批量提交(每1000条)if (dataList.size() >= 1000) {batchInsert(targetTable, columnMapping, dataList);dataList.clear();}})).sheet().doRead();// 处理剩余数据if (!dataList.isEmpty()) {batchInsert(targetTable, columnMapping, dataList);}}/*** 批量插入(使用预编译,防止参数注入)*/private void batchInsert(String targetTable, Map<String, String> columnMapping, List<Map<String, Object>> dataList) {// 构建插入SQL:INSERT INTO user (name, age, phone) VALUES (?, ?, ?)List<String> dbColumns = new ArrayList<>(columnMapping.values());String columns = String.join(",", dbColumns);String placeholders = String.join(",", Collections.nCopies(dbColumns.size(), "?"));String sql = String.format("INSERT INTO %s (%s) VALUES (%s)", targetTable, columns, placeholders);try (Connection conn = dataSource.getConnection()) {conn.setAutoCommit(false); // 关闭自动提交PreparedStatement ps = conn.prepareStatement(sql);// 批量设置参数(预编译核心)for (Map<String, Object> row : dataList) {for (int i = 0; i < dbColumns.size(); i++) {String column = dbColumns.get(i);Object value = row.get(column);ps.setObject(i + 1, value);}ps.addBatch(); // 添加到批处理}// 执行批处理ps.executeBatch();conn.commit(); // 提交事务} catch (SQLException e) {throw new RuntimeException("批量插入失败", e);}}/*** 解析Excel行数据,映射为数据库字段值*/private Map<String, Object> parseExcelRow(Map<Integer, String> excelRow, Map<String, String> columnMapping) {Map<String, Object> rowData = new HashMap<>();// 假设Excel表头行是第一行,这里简化处理(实际需先读取表头)excelRow.forEach((index, value) -> {// 根据列索引获取Excel列名(实际需先解析表头映射)String excelColumnName = getExcelColumnNameByIndex(index);String dbColumn = columnMapping.get(excelColumnName);if (dbColumn != null) {rowData.put(dbColumn, value);}});return rowData;}/*** 模拟:根据列索引获取Excel列名(实际需从Excel表头解析)*/private String getExcelColumnNameByIndex(Integer index) {Map<Integer, String> indexToName = new HashMap<>();indexToName.put(0, "姓名");indexToName.put(1, "年龄");indexToName.put(2, "手机号");return indexToName.get(index);}/*** 批量更新扩展(核心思路同批量插入)*/private void batchUpdate(String targetTable, Map<String, String> columnMapping, List<Map<String, Object>> dataList) {// 假设主键为id,构建更新SQL:UPDATE user SET name=?, age=? WHERE id=?List<String> dbColumns = new ArrayList<>(columnMapping.values());dbColumns.remove("id"); // 移除主键列String updateSet = dbColumns.stream().map(col -> col + "=?").collect(Collectors.joining(","));String sql = String.format("UPDATE %s SET %s WHERE id=?", targetTable, updateSet);// 后续逻辑同批量插入,仅参数设置顺序不同(先设置更新列,再设置主键)try (Connection conn = dataSource.getConnection()) {conn.setAutoCommit(false);PreparedStatement ps = conn.prepareStatement(sql);for (Map<String, Object> row : dataList) {int paramIndex = 1;// 设置更新列参数for (String column : dbColumns) {ps.setObject(paramIndex++, row.get(column));}// 设置主键参数(WHERE条件)ps.setObject(paramIndex, row.get("id"));ps.addBatch();}ps.executeBatch();conn.commit();} catch (SQLException e) {throw new RuntimeException("批量更新失败", e);}}}
3.5 控制器层(测试接口)
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;/*** 报表查询接口*/@PostMapping("/report/{reportCode}")public List<Map<String, Object>> queryReport(@PathVariable String reportCode,@RequestBody Map<String, Object> params) {return reportService.executeReport(reportCode, params);}/*** Excel导入接口*/@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();}}}
四、关键安全保障说明
参数预编译:所有前端传递的参数均通过PreparedStatement.setObject()设置,MyBatis/JDBC自动做参数转义,彻底防止SQL注入
表名/字段名白名单:对配置的表名做白名单校验,仅允许导入到指定表,避免恶意配置user; drop table xxx等危险表名
SQL模板校验:可扩展添加SQL模板语法校验,禁止配置包含;、DROP、ALTER等危险关键字的SQL
权限控制:建议为报表配置/Excel导入配置添加权限校验,仅允许管理员配置SQL和导入规则
五、总结
预编译核心:将配置的SQL模板中的#{参数}替换为?,通过PreparedStatement设置参数,保留JDBC预编译能力,杜绝参数注入
配置化安全:表名/字段名通过白名单校验,仅允许合法配置,防止表名注入
批量操作优化:使用JDBC批处理(addBatch()+executeBatch())提升Excel导入/更新效率,结合事务保证数据一致性
该方案完全利用MyBatis/JDBC的预编译特性,同时满足报表SQL和Excel导入的全配置化需求,兼顾安全性和灵活性,可直接集成到SpringBoot项目中使用。
(注:文档部分内容可能由 AI 生成)
