个人随笔
目录
MyBatis+Spring 动态配置SQL(可运行示例+笔记)
2026-01-30 11:52:58

一、前言

本文针对 MyBatis+Spring 环境,整理「动态配置SQL」的可运行示例(基于用户提供的核心代码优化),详细拆解实现步骤,分析该方案的优缺点,并补充系统启动初始化、SQL注入防护等关键注意事项;同时新增用户提供的「通用Mapper+Provider」模式,对比两种动态SQL实现方式的差异与优劣,最终整理为可直接参考的技术笔记,适配实际开发场景。

核心需求:SQL语句不写死在 Mapper.xml/注解中,而是通过配置(如数据库、配置文件)动态获取,运行时动态执行,实现SQL的灵活配置与动态调整;后续补充的「通用Mapper+Provider」模式,更是优化了实现逻辑,解决了初始化繁琐的问题。

二、可运行示例(两种实现模式)

本文提供两种 MyBatis+Spring 动态配置SQL的可运行实现,均基于 Spring + MyBatis 整合环境(需提前完成基础整合:数据源、SqlSessionFactory、SqlSessionTemplate 配置),分别适配不同业务场景,可根据需求选择使用。

模式一:原生API模式(基于MyBatis底层API,动态注册MappedStatement)

该模式基于 MyBatis 底层 API(Configuration、MappedStatement),动态注册SQL语句到MyBatis配置中,再执行查询,优化后避免重复注册、增加异常处理,适配系统启动初始化场景。

2.1.1 依赖准备(pom.xml 核心依赖)

确保引入 Spring、MyBatis 及整合依赖,版本可根据项目实际调整:

  1. <!-- Spring 核心依赖 -->
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-context</artifactId>
  5. <version>5.3.20</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-jdbc</artifactId>
  10. <version>5.3.20</version>
  11. </dependency>
  12. <!-- MyBatis 核心依赖 -->
  13. <dependency>
  14. <groupId>org.mybatis</groupId>
  15. <artifactId>mybatis</artifactId>
  16. <version>3.5.10</version>
  17. </dependency>
  18. <!-- MyBatis + Spring 整合依赖 -->
  19. <dependency>
  20. <groupId>org.mybatis</groupId>
  21. <artifactId>mybatis-spring</artifactId>
  22. <version>2.0.7</version>
  23. </dependency>
  24. <!-- 数据库驱动(以MySQL为例) -->
  25. <dependency>
  26. <groupId>mysql</groupId>
  27. <artifactId>mysql-connector-java</artifactId>
  28. <version>8.0.30</version>
  29. </dependency>
  30. <!-- 连接池(可选,推荐) -->
  31. <dependency>
  32. <groupId>com.alibaba</groupId>
  33. <artifactId>druid</artifactId>
  34. <version>1.2.16</version>
  35. </dependency>

2.1.2 核心实现类(DynamicSqlService.java)

封装动态SQL的注册、执行方法,优化点:增加重复注册校验、异常处理、参数校验,适配系统启动初始化场景:

  1. import org.apache.ibatis.session.SqlSession;
  2. import org.apache.ibatis.session.SqlSessionFactory;
  3. import org.apache.ibatis.mapping.*;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.stereotype.Service;
  6. import java.util.ArrayList;
  7. import java.util.LinkedHashMap;
  8. import java.util.List;
  9. import java.util.Map;
  10. @Service
  11. public class DynamicSqlService {
  12. // 注入MyBatis核心工厂(Spring整合后自动配置,可直接注入)
  13. @Autowired
  14. private SqlSessionFactory sqlSessionFactory;
  15. /**
  16. * 动态注册SQL到MyBatis(核心方法)
  17. * @param configKey 配置键(如:USER_LIST_SQL),用于拼接MappedStatement的唯一ID
  18. * @param configValue 配置的SQL语句(如:select * from user where id = #{id})
  19. */
  20. public void registerDynamicSql(String configKey, String configValue) {
  21. // 1. 校验参数:避免空值导致异常
  22. if (configKey == null || configKey.isEmpty() || configValue == null || configValue.isEmpty()) {
  23. throw new IllegalArgumentException("configKey和configValue不能为空!");
  24. }
  25. // 2. 仅处理后缀为_SQL的配置(与用户原逻辑一致)
  26. if (configKey.indexOf("_SQL") == -1) {
  27. return;
  28. }
  29. // 3. 获取MyBatis全局配置对象(Configuration是单例,存储所有映射配置)
  30. SqlSession sqlSession = sqlSessionFactory.openSession(false); // 非自动提交,仅用于获取配置
  31. Configuration configuration = sqlSession.getConfiguration();
  32. // 4. 构建MappedStatement的唯一ID(遵循MyBatis原生规则:Mapper全限定名+配置键)
  33. String mappedStatementId = "com.gdpost.acc.service.mapper.WorkbenchMapper." + configKey;
  34. // 5. 校验:避免重复注册(重复注册会抛出异常)
  35. if (configuration.hasStatement(mappedStatementId, false)) {
  36. sqlSession.close();
  37. return; // 已注册,直接返回
  38. }
  39. try {
  40. // 6. 构建ResultMap:适配动态SQL(返回字段不固定,用LinkedHashMap接收,保留字段顺序)
  41. List<ResultMap> resultMaps = new ArrayList<>();
  42. ResultMap resultMap = new ResultMap.Builder(
  43. configuration,
  44. mappedStatementId, // ResultMap的唯一ID(与MappedStatement ID一致即可)
  45. LinkedHashMap.class, // 结果类型:LinkedHashMap
  46. new ArrayList<ResultMapping>() // 无显式字段映射(动态字段适配)
  47. ).build();
  48. resultMaps.add(resultMap);
  49. // 7. 构建SqlSource:将配置的SQL字符串解析为MyBatis可执行的SQL源
  50. // 核心:使用MyBatis默认脚本语言(支持#{ }参数预编译,防止SQL注入)
  51. SqlSource sqlSource = configuration.getDefaultScriptingLanguageInstance()
  52. .createSqlSource(configuration, configValue, Map.class);
  53. // 8. 构建MappedStatement(MyBatis执行SQL的核心载体,等价于xml中的<select>标签)
  54. MappedStatement mappedStatement = new MappedStatement.Builder(
  55. configuration,
  56. mappedStatementId, // 唯一ID
  57. sqlSource, // 解析后的SQL源
  58. SqlCommandType.SELECT // SQL命令类型(此处仅适配查询,可扩展INSERT/UPDATE/DELETE)
  59. ).resultMaps(resultMaps) // 设置结果映射
  60. .build();
  61. // 9. 将MappedStatement注册到MyBatis配置中,使其生效
  62. configuration.addMappedStatement(mappedStatement);
  63. } catch (Exception e) {
  64. throw new RuntimeException("动态注册SQL失败!configKey:" + configKey, e);
  65. } finally {
  66. // 关闭SqlSession(仅用于获取配置,未执行事务操作)
  67. sqlSession.close();
  68. }
  69. }
  70. /**
  71. * 执行动态注册的SQL(查询方法)
  72. * @param moduleNo 模块编号(如:USER_LIST),用于拼接configKey(moduleNo + "_SQL")
  73. * @param paramMap 查询参数(SQL中#{key}对应的参数,存放在Map中)
  74. * @return 查询结果:List<LinkedHashMap>,每个Map对应一行数据(key=字段名,value=字段值)
  75. */
  76. public List<LinkedHashMap<String, Object>> executeDynamicSql(String moduleNo, Map<String, Object> paramMap) {
  77. // 1. 拼接configKey(与注册时的configKey规则一致)
  78. String configKey = moduleNo + "_SQL";
  79. // 2. 拼接MappedStatement ID(与注册时的ID完全一致)
  80. String mappedStatementId = "com.gdpost.acc.service.mapper.WorkbenchMapper." + configKey;
  81. // 3. 获取SqlSession(Spring整合后,可注入SqlSessionTemplate,无需手动关闭)
  82. try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) { // 自动提交(查询无需事务,可简化)
  83. // 4. 执行查询:通过MappedStatement ID定位SQL,传入参数Map
  84. return sqlSession.selectList(mappedStatementId, paramMap);
  85. } catch (Exception e) {
  86. throw new RuntimeException("执行动态SQL失败!moduleNo:" + moduleNo, e);
  87. }
  88. }
  89. }

2.1.3 系统启动初始化(关键:提前注册动态SQL)

该模式需在系统启动时初始化,加载SQL配置并注册,避免首次执行时注册耗时或失败。以下提供2种常用初始化方式:

方式1:Spring 启动时初始化(CommandLineRunner)

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.boot.CommandLineRunner;
  3. import org.springframework.stereotype.Component;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. /**
  7. * 系统启动时初始化:加载SQL配置,注册动态SQL
  8. * (适用于Spring Boot项目,Spring MVC项目可使用InitializingBean接口)
  9. */
  10. @Component
  11. public class DynamicSqlInitializer implements CommandLineRunner {
  12. @Autowired
  13. private DynamicSqlService dynamicSqlService;
  14. // 模拟:从数据库/配置文件读取的SQL配置(实际开发中替换为真实配置加载逻辑)
  15. private Map<String, String> sqlConfigMap = new HashMap<>() {{
  16. // key:configKey(后缀_SQL),value:配置的SQL语句(使用#{ }参数,防止注入)
  17. put("USER_LIST_SQL", "select id, username, age from user where age > #{minAge}");
  18. put("ORDER_LIST_SQL", "select order_no, create_time, amount from order where create_time > #{startTime}");
  19. }};
  20. @Override
  21. public void run(String... args) throws Exception {
  22. // 系统启动时,批量注册所有动态SQL
  23. for (Map.Entry<String, String> entry : sqlConfigMap.entrySet()) {
  24. dynamicSqlService.registerDynamicSql(entry.getKey(), entry.getValue());
  25. }
  26. System.out.println("动态SQL初始化注册完成!共注册:" + sqlConfigMap.size() + "条");
  27. }
  28. }

方式2:Spring MVC 初始化(InitializingBean)

  1. import org.springframework.beans.factory.InitializingBean;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.stereotype.Component;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. @Component
  7. public class DynamicSqlInitializer implements InitializingBean {
  8. @Autowired
  9. private DynamicSqlService dynamicSqlService;
  10. private Map<String, String> sqlConfigMap = new HashMap<>() {{
  11. put("USER_LIST_SQL", "select id, username, age from user where age > #{minAge}");
  12. put("ORDER_LIST_SQL", "select order_no, create_time, amount from order where create_time > #{startTime}");
  13. }};
  14. // 初始化方法:Bean初始化完成后执行
  15. @Override
  16. public void afterPropertiesSet() throws Exception {
  17. for (Map.Entry<String, String> entry : sqlConfigMap.entrySet()) {
  18. dynamicSqlService.registerDynamicSql(entry.getKey(), entry.getValue());
  19. }
  20. }
  21. }

2.1.4 测试使用(Controller层示例)

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.web.bind.annotation.GetMapping;
  3. import org.springframework.web.bind.annotation.RequestParam;
  4. import org.springframework.web.bind.annotation.RestController;
  5. import java.util.LinkedHashMap;
  6. import java.util.List;
  7. import java.util.Map;
  8. @RestController
  9. public class DynamicSqlController {
  10. @Autowired
  11. private DynamicSqlService dynamicSqlService;
  12. // 测试:执行动态SQL(查询用户列表)
  13. @GetMapping("/user/list")
  14. public List<LinkedHashMap<String, Object>> getUserList(@RequestParam Integer minAge) {
  15. // 1. 构建查询参数(对应SQL中的#{minAge})
  16. Map<String, Object> paramMap = Map.of("minAge", minAge);
  17. // 2. 执行动态SQL:moduleNo = "USER_LIST",对应configKey = "USER_LIST_SQL"
  18. return dynamicSqlService.executeDynamicSql("USER_LIST", paramMap);
  19. }
  20. // 测试:执行动态SQL(查询订单列表)
  21. @GetMapping("/order/list")
  22. public List<LinkedHashMap<String, Object>> getOrderList(@RequestParam String startTime) {
  23. Map<String, Object> paramMap = Map.of("startTime", startTime);
  24. return dynamicSqlService.executeDynamicSql("ORDER_LIST", paramMap);
  25. }
  26. }

模式二:通用Mapper+Provider模式(推荐,无需初始化,修改直接生效)

该模式是用户提供的优化方案,通过 MyBatis 原生的 @SelectProvider@InsertProvider 注解,结合 SQL 构建器(Provider)实现动态SQL,无需手动注册 MappedStatement,无需系统启动初始化,SQL配置修改后直接生效,更贴合 MyBatis 开发规范,适配报表查询、批量导入等场景。

2.2.1 核心代码实现(通用Mapper+SQL构建器)

该模式分为「通用Mapper接口」和「SQL构建器(Provider)」两部分,复用 MyBatis 预编译能力,参数仍用 #{} 包裹,保障SQL注入防护,同时简化开发流程。

1. 通用动态SQL执行Mapper(DynamicSqlMapper.java)

定义通用接口,统一处理动态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. * 无需动态注册MappedStatement,MyBatis自动解析Provider构建SQL
  9. */
  10. public interface DynamicSqlMapper {
  11. /**
  12. * 执行动态查询SQL(报表查询核心方法)
  13. * @param sqlTemplate 从数据库/配置文件加载的SQL模板(含#{参数},支持预编译)
  14. * @param params 前端传递的查询参数(与SQL模板中的#{参数}对应)
  15. * @return 查询结果(Map列表,key为数据库字段名,value为字段值)
  16. */
  17. @SelectProvider(type = DynamicSqlProvider.class, method = "buildQuerySql")
  18. List<Map<String, Object>> executeDynamicQuery(
  19. @Param("sqlTemplate") String sqlTemplate,
  20. @Param("params") Map<String, Object> params);
  21. /**
  22. * 批量插入(Excel导入核心方法)
  23. * @param tableName 目标表名(需提前做白名单校验,防止表名注入)
  24. * @param columns 数据库字段列表(固定配置,避免动态拼接字段名)
  25. * @param dataList 导入数据列表(Map的key为字段名,value为对应字段值)
  26. * @return 影响行数(批量插入的总条数)
  27. */
  28. @InsertProvider(type = DynamicSqlProvider.class, method = "buildBatchInsertSql")
  29. int batchInsert(
  30. @Param("tableName") String tableName,
  31. @Param("columns") List<String> columns,
  32. @Param("dataList") List<Map<String, Object>> dataList);
  33. }

2. SQL构建器(DynamicSqlProvider.java)

通过 Provider 动态构建 SQL 语句,复用 MyBatis 预编译机制,无需手动解析 SQL,参数自动适配,同时支持批量插入场景:

  1. import org.apache.ibatis.jdbc.SQL;
  2. import java.util.List;
  3. import java.util.Map;
  4. /**
  5. * SQL构建器:动态拼接SQL,兼容MyBatis预编译机制,无需手动注册配置
  6. * 核心优势:SQL模板直接从配置读取,修改后无需重启、无需初始化,直接生效
  7. */
  8. public class DynamicSqlProvider {
  9. /**
  10. * 构建动态查询SQL(直接返回配置中的SQL模板,MyBatis自动解析预编译)
  11. * @param paramMap 封装了@Param传递的sqlTemplate和params参数
  12. * @return 配置中的SQL模板(MyBatis自动解析#{params.xxx}并完成预编译)
  13. */
  14. public String buildQuerySql(Map<String, Object> paramMap) {
  15. // 从paramMap中获取@Param注解传递的参数(sqlTemplate和params)
  16. String sqlTemplate = (String) paramMap.get("sqlTemplate");
  17. // 直接返回SQL模板,MyBatis会自动解析模板中的#{params.xxx},并对参数进行预编译
  18. return sqlTemplate;
  19. }
  20. /**
  21. * 构建批量插入SQL(表名、字段名拼接+参数预编译,兼顾效率与安全)
  22. * @param paramMap 封装了@Param传递的tableName、columns、dataList参数
  23. * @return 批量插入SQL语句(参数用#{dataList[索引].字段名}包裹,支持预编译)
  24. */
  25. public String buildBatchInsertSql(Map<String, Object> paramMap) {
  26. // 1. 从paramMap中获取参数(提前校验,避免空值)
  27. String tableName = (String) paramMap.get("tableName");
  28. List<String> columns = (List<String>) paramMap.get("columns");
  29. List<Map<String, Object>> dataList = (List<Map<String, Object>>) paramMap.get("dataList");
  30. // 2. 拼接批量插入SQL框架(使用MyBatis的SQL工具类,避免手动拼接语法错误)
  31. SQL sql = new SQL().INSERT_INTO(tableName);
  32. columns.forEach(sql::INTO_COLUMNS); // 批量添加字段列表(INTO table (col1, col2, ...))
  33. // 3. 批量添加值列表:每个值用#{dataList[索引].字段名}包裹,保证MyBatis能预编译参数
  34. for (int i = 0; i < dataList.size(); i++) {
  35. StringBuilder values = new StringBuilder();
  36. for (int j = 0; j < columns.size(); j++) {
  37. String column = columns.get(j);
  38. // 拼接参数占位符:#{dataList[i].column},MyBatis自动匹配数据并预编译
  39. values.append("# {dataList[").append(i).append("].").append(column).append("}");
  40. if (j < columns.size() - 1) {
  41. values.append(",");
  42. }
  43. }
  44. // 为每条数据添加VALUES子句(VALUES (#{...}, #{...}, ...))
  45. sql.VALUES(String.join(",", columns), values.toString());
  46. }
  47. // 4. 返回拼接完成的批量插入SQL(MyBatis自动预编译所有参数,防止注入)
  48. return sql.toString();
  49. }
  50. }

2.2.2 测试使用(Service+Controller层示例)

该模式使用更简洁,无需初始化操作,直接注入 DynamicSqlMapper 即可执行动态SQL,修改SQL配置后无需重启项目,直接生效。

1. Service层封装(DynamicSqlBizService.java)

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.stereotype.Service;
  3. import java.util.List;
  4. import java.util.Map;
  5. @Service
  6. public class DynamicSqlBizService {
  7. // 直接注入通用Mapper,无需手动注册MappedStatement
  8. @Autowired
  9. private DynamicSqlMapper dynamicSqlMapper;
  10. /**
  11. * 执行动态查询(报表查询)
  12. * @param sqlTemplateCode SQL模板编码(用于从数据库/配置中心获取SQL模板)
  13. * @param params 查询参数
  14. * @return 查询结果列表
  15. */
  16. public List<Map<String, Object>> doDynamicQuery(String sqlTemplateCode, Map<String, Object> params) {
  17. // 1. 从数据库/配置中心获取SQL模板(实际开发中替换为真实配置加载逻辑)
  18. // 示例:模拟从配置中获取SQL模板,修改该模板后无需重启,下次执行直接生效
  19. String sqlTemplate = getSqlTemplateFromConfig(sqlTemplateCode);
  20. // 2. 调用通用Mapper执行动态查询(MyBatis自动预编译参数)
  21. return dynamicSqlMapper.executeDynamicQuery(sqlTemplate, params);
  22. }
  23. /**
  24. * 批量插入(Excel导入)
  25. * @param tableName 目标表名(已做白名单校验)
  26. * @param columns 字段列表
  27. * @param dataList 导入数据
  28. * @return 影响行数
  29. */
  30. public int doBatchInsert(String tableName, List<String> columns, List<Map<String, Object>> dataList) {
  31. // 直接调用通用Mapper的批量插入方法,Provider自动构建SQL并预编译
  32. return dynamicSqlMapper.batchInsert(tableName, columns, dataList);
  33. }
  34. /**
  35. * 模拟:从配置中心/数据库获取SQL模板
  36. * 核心特点:修改配置中的SQL模板后,无需重启项目、无需初始化,下次调用直接生效
  37. */
  38. private String getSqlTemplateFromConfig(String sqlTemplateCode) {
  39. // 实际开发中:通过sqlTemplateCode查询数据库/配置中心,获取对应的sqlTemplate
  40. // 示例:模拟不同模板编码对应的SQL模板
  41. return switch (sqlTemplateCode) {
  42. case "USER_LIST" -> "select id, username, age from user where age > #{params.minAge}";
  43. case "ORDER_LIST" -> "select order_no, create_time, amount from order where create_time > #{params.startTime}";
  44. default -> throw new RuntimeException("未找到对应的SQL模板:" + sqlTemplateCode);
  45. };
  46. }
  47. }

2. Controller层测试

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.web.bind.annotation.GetMapping;
  3. import org.springframework.web.bind.annotation.RequestParam;
  4. import org.springframework.web.bind.annotation.RestController;
  5. import java.util.List;
  6. import java.util.Map;
  7. @RestController
  8. public class DynamicSqlProviderController {
  9. @Autowired
  10. private DynamicSqlBizService dynamicSqlBizService;
  11. // 测试:动态查询(报表查询)
  12. @GetMapping("/provider/user/list")
  13. public List<Map<String, Object>> getUserListByProvider(@RequestParam Integer minAge) {
  14. Map<String, Object> params = Map.of("minAge", minAge);
  15. // 传入SQL模板编码,无需关心SQL注册、初始化,修改模板后直接生效
  16. return dynamicSqlBizService.doDynamicQuery("USER_LIST", params);
  17. }
  18. }

2.2.3 关键配置补充(MyBatis核心配置)

该模式无需额外配置,只需确保 MyBatis 能扫描到 DynamicSqlMapper 接口,Spring Boot 项目可通过注解扫描自动适配,application.yml 配置如下:

  1. spring:
  2. datasource:
  3. driver-class-name: com.mysql.cj.jdbc.Driver
  4. url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
  5. username: root
  6. password: 123456
  7. type: com.alibaba.druid.pool.DruidDataSource
  8. mybatis:
  9. # 配置Mapper接口扫描路径(确保DynamicSqlMapper能被MyBatis扫描到)
  10. mapper-locations: classpath:com/gdpost/acc/service/mapper/*.java
  11. configuration:
  12. # 开启驼峰命名映射(可选,方便Map接收字段时适配Java命名规范)
  13. map-underscore-to-camel-case: true
  14. # 日志打印(可选,用于调试动态SQL)
  15. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

2.3 两种模式核心差异对比(重点)

结合你的疑问——「Provider模式是不是更好,不用初始化,修改后直接生效」,这里明确两种模式的核心差异,帮你清晰判断适用场景:

对比维度 模式一:原生API模式(动态注册MappedStatement) 模式二:通用Mapper+Provider模式(推荐)
核心实现 基于MyBatis底层API(Configuration、MappedStatement),动态注册SQL到全局配置 基于MyBatis原生@SelectProvider/@InsertProvider注解,Provider动态构建SQL,无需注册
是否需要初始化 是(必须系统启动时批量注册,否则首次执行耗时/失败;修改SQL后需重新注册/重启) 否(无需初始化、无需注册,SQL模板从配置动态获取,修改后直接生效)
开发复杂度 高(需手动处理MappedStatement注册、ResultMap构建、资源关闭,易出错) 低(贴合MyBatis开发规范,只需定义Mapper和Provider,复用原生机制)
SQL修改生效方式 修改后需重新注册MappedStatement,或重启项目才能生效 修改配置中的SQL模板后,无需重启、无需额外操作,下次执行直接生效
SQL注入防护 支持(需确保SQL模板用#{ },手动解析SqlSource) 支持(MyBatis自动预编译,Provider构建SQL时强制用#{ },防护更彻底)
适配场景 特殊场景(需动态修改SQL执行类型、ResultMap,或深度定制MappedStatement) 通用场景(报表查询、批量导入、SQL频繁变更,追求简洁高效)
维护成本 高(需维护注册逻辑、初始化代码,排查问题依赖底层API调试) 低(代码简洁、贴合规范,调试方便,SQL模板与代码分离,易维护)

结论:模式二(通用Mapper+Provider)更优,完全解决了模式一“需要初始化、修改不直接生效”的痛点,同时开发更简洁、维护更方便,更贴合MyBatis原生开发规范,适合绝大多数动态SQL场景(如你提到的可配置报表、批量导入)。

三、实现步骤拆解(详细版)

分别拆解两种模式的实现步骤,每个步骤明确目的、操作和注意事项,确保可复现,重点突出Provider模式的简洁性。

3.1 模式一:原生API模式(动态注册MappedStatement)步骤

步骤1:环境准备(基础前提)

  1. 完成 Spring + MyBatis 基础整合,确保数据源、SqlSessionFactory 能正常初始化(核心:SqlSessionFactory 是获取 MyBatis 配置的入口);

  2. 引入所需依赖(MyBatis、Spring、数据库驱动),避免依赖缺失导致的异常;

  3. 确认SQL配置来源(数据库、配置文件等),提前定义 configKey(后缀_SQL)和 configValue(SQL语句)的对应关系。

步骤2:封装动态SQL注册方法(核心步骤)

  1. 参数校验:校验 configKey 和 configValue 非空,仅处理后缀为_SQL的配置(与业务规则一致);

  2. 获取 MyBatis 全局配置(Configuration):通过 SqlSessionFactory 打开 SqlSession,再从 SqlSession 中获取 Configuration(Configuration 是单例,存储所有映射信息);

  3. 构建 MappedStatement 唯一ID:遵循 MyBatis 原生规则(Mapper接口全限定名 + configKey),确保ID全局唯一(避免重复注册);

  4. 重复注册校验:通过 configuration.hasStatement() 判断该ID是否已注册,已注册则直接返回,避免抛出异常;

  5. 构建 ResultMap:由于动态SQL的返回字段不固定,使用 LinkedHashMap 作为结果类型,无需显式配置字段映射(适配任意返回字段);

  6. 构建 SqlSource:通过 MyBatis 默认脚本语言驱动,将 configValue(SQL字符串)解析为 SqlSource(MyBatis 可执行的SQL载体),核心支持 #{ } 参数预编译;

  7. 构建 MappedStatement:通过 MappedStatement.Builder 封装 ID、SqlSource、命令类型(SELECT)、ResultMap 等信息,等价于 XML 中的 <select>标签;

  8. 注册 MappedStatement:调用 configuration.addMappedStatement(),将动态构建的 MappedStatement 注册到 MyBatis 配置中,使其生效;

  9. 异常处理+资源关闭:捕获注册过程中的异常,统一抛出便于排查;关闭 SqlSession(仅用于获取配置,无事务操作)。

步骤3:封装动态SQL执行方法

  1. 拼接参数:根据 moduleNo 拼接 configKey(moduleNo + “_SQL”),再拼接 MappedStatement ID(与注册时的ID完全一致);

  2. 获取 SqlSession:通过 SqlSessionFactory 打开 SqlSession(自动提交模式,查询无需事务);

  3. 执行查询:调用 sqlSession.selectList(),传入 MappedStatement ID 和参数 Map,返回 List<LinkedHashMap> 结果集;

  4. 异常处理:捕获执行过程中的异常(如SQL语法错误、参数缺失),统一封装便于排查。

步骤4:系统启动初始化(关键优化)

  1. 选择初始化方式(CommandLineRunner 或 InitializingBean),确保系统启动时执行初始化逻辑;

  2. 加载SQL配置:从数据库/配置文件中读取所有 configKey 和 configValue,存储到 Map 中;

  3. 批量注册:循环调用注册方法,将所有动态SQL提前注册到 MyBatis 配置中,避免首次执行时注册耗时。

步骤5:测试验证

  1. 编写测试接口(Controller),构建查询参数,调用执行方法;

  2. 启动项目,查看初始化日志,确认动态SQL注册成功;

  3. 调用测试接口,验证查询结果是否正确,同时检查日志中的SQL是否被预编译(确认 #{ } 参数生效)。

3.2 模式二:通用Mapper+Provider模式(推荐)步骤

该模式步骤简洁,无需初始化,核心是“定义Mapper+编写Provider+直接使用”,全程贴合MyBatis开发规范。

步骤1:环境准备(基础前提)

  1. 完成 Spring + MyBatis 基础整合,确保数据源、SqlSessionFactory 正常初始化;

  2. 引入核心依赖(与模式一一致,无需额外依赖);

  3. 确认SQL模板来源(数据库/配置中心),定义模板编码与SQL模板的对应关系(便于动态获取)。

步骤2:定义通用DynamicSqlMapper接口

  1. 使用 @SelectProvider 注解定义动态查询方法,指定 Provider 类和构建SQL的方法;

  2. 使用 @InsertProvider 注解定义批量插入方法,同样指定 Provider 类和构建方法;

  3. 通过 @Param 注解传递参数(sqlTemplate、params、tableName等),确保Provider能正常获取参数;

  4. 明确方法返回值(查询返回 List<Map>,批量插入返回int),适配业务需求。

步骤3:编写DynamicSqlProvider构建器

  1. 编写 buildQuerySql 方法:从参数Map中获取SQL模板,直接返回(MyBatis自动解析预编译);

  2. 编写 buildBatchInsertSql 方法:使用 MyBatis 的 SQL 工具类拼接批量插入SQL,参数用 #{dataList[索引].字段名} 包裹,确保预编译;

  3. 添加参数校验(可选),避免空表名、空字段列表导致的SQL语法错误。

步骤4:封装Service层(可选,推荐)

  1. 注入 DynamicSqlMapper 接口(Spring自动扫描,无需手动实例化);

  2. 封装业务方法:从配置中获取SQL模板,调用Mapper的方法执行动态SQL;

  3. 添加异常处理、参数校验(如SQL模板编码校验、批量插入数据非空校验)。

步骤5:测试验证

  1. 编写Controller测试接口,注入Service层,调用业务方法;

  2. 启动项目(无需初始化操作),直接调用接口,验证查询/批量插入功能;

  3. 修改配置中的SQL模板,再次调用接口,验证修改后是否直接生效(无需重启项目)。

四、方案优缺点分析(重点)

分别分析两种模式的优缺点,重点呼应你的疑问,明确 Provider 模式的优势,同时客观说明两种模式的适用场景。

4.1 模式一:原生API模式(动态注册MappedStatement)

优点

  1. 灵活性极高:可深度定制 MappedStatement(如修改SQL执行类型、自定义ResultMap、设置缓存等),适配特殊业务场景;

  2. 完全脱离XML/注解:无需编写Mapper接口和XML,全程通过API动态构建,适合无固定Mapper的场景;

  3. 支持任意动态SQL:只要能构建出合法的SqlSource,可支持任意复杂的SQL语句(前提是手动处理解析逻辑)。

缺点(核心痛点)

  1. 必须初始化:需在系统启动时批量注册MappedStatement,否则首次执行耗时,甚至失败;

  2. 修改不直接生效:SQL模板修改后,需重新注册MappedStatement或重启项目,无法实时生效;

  3. 开发复杂度高:需手动处理Configuration、MappedStatement、SqlSource的构建,还要关注资源关闭、重复注册等问题,易出错;

  4. 维护成本高:底层API调试困难,后期修改注册逻辑、适配新场景时,成本较高;

  5. 存在并发风险:若运行时动态修改SQL,未加锁控制,可能出现重复注册异常。

4.2 模式二:通用Mapper+Provider模式(推荐)

优点(核心优势,呼应你的疑问)

  1. 无需初始化:彻底解决模式一的痛点,无需系统启动注册,无需额外初始化代码,项目启动后可直接使用;

  2. 修改直接生效:SQL模板从配置动态获取,修改配置后,下次调用接口直接生效,无需重启项目、无需重新注册;

  3. 开发简洁:贴合MyBatis原生开发规范,只需定义Mapper和Provider,复用MyBatis预编译机制,无需手动处理底层API;

  4. SQL注入防护更彻底:MyBatis自动解析SQL模板中的#{ }参数,Provider构建批量插入SQL时强制使用预编译占位符,避免注入风险;

  5. 维护成本低:代码简洁、逻辑清晰,SQL模板与业务代码分离,调试方便,后期扩展(如新增批量更新)更简单;

  6. 通用性强:适配报表查询、批量导入等绝大多数动态SQL场景,可直接复用Mapper接口,无需重复开发。

缺点(相对轻微)

  1. 定制化能力弱:无法深度定制MappedStatement(如自定义缓存、特殊ResultMap),适合通用场景,不适合特殊定制需求;

  2. 依赖MyBatis注解:需遵循@SelectProvider/@InsertProvider的使用规范,若项目未使用MyBatis注解,需额外适配;

  3. 表名字段需校验:批量插入时,tableName和columns需提前校验(白名单),否则可能存在表名/字段名注入风险(可通过业务逻辑规避)。

4.3 两种模式选择建议

  1. 优先选择模式二(通用Mapper+Provider):若你的场景是「可配置报表查询、批量导入、SQL频繁变更」,追求简洁、高效、易维护,且无需深度定制MappedStatement,该模式是最优选择;

  2. 选择模式一(原生API):仅当业务需要「深度定制MappedStatement」(如动态修改缓存策略、自定义ResultMap适配特殊返回格式),且能接受初始化、修改不实时生效的痛点时,才考虑使用;

  3. 补充说明:你的判断完全正确——「Provider模式更好」,它完美解决了原生API模式“需要初始化、修改不直接生效”的核心痛点,同时更贴合MyBatis开发规范,是实际开发中动态SQL的首选方案。

五、关键注意事项(避坑重点)

5.1 SQL注入防护(核心重点)

两种模式均支持SQL注入防护,但需注意以下细节,避免防护失效:

  1. 参数占位符:无论哪种模式,SQL模板中的参数必须使用 #{ } 占位符,禁止使用 ${ }(${ } 会直接拼接字符串,导致注入);

  2. 表名/字段名防护:批量插入时,tableName和columns需做白名单校验(如定义允许的表名列表,禁止动态拼接未知表名/字段名);

  3. SQL模板校验:从配置中获取的SQL模板,需校验是否包含drop、alter、delete等高危关键字(根据业务场景调整),避免恶意修改模板;

  4. 参数校验:对前端传递的params参数进行校验(如数字类型参数校验是否为有效数字、字符串参数过滤特殊字符),双重防护。

5.2 模式一专属注意事项(原生API模式)

  1. 重复注册问题:MyBatis的Configuration中,MappedStatement的ID是全局唯一的,注册前必须通过configuration.hasStatement()校验,避免重复注册抛出异常;

  2. 初始化时机:初始化逻辑需在SqlSessionFactory完全初始化后执行(CommandLineRunner/InitializingBean可满足),避免获取不到Configuration;

  3. 资源关闭:通过SqlSession获取Configuration后,必须关闭SqlSession(无需提交事务),避免资源泄露;

  4. SQL模板更新:若需修改SQL模板,需先删除已注册的MappedStatement(configuration.getMappedStatements().removeIf()),再重新注册。

5.3 模式二专属注意事项(Provider模式)

  1. Mapper扫描:确保MyBatis能扫描到DynamicSqlMapper接口(通过mybatis.mapper-locations配置扫描路径),否则会出现“找不到Mapper”异常;

  2. 参数传递:Provider方法的参数必须是Map<String, Object>,且参数名需与@Param注解的名称一致,否则无法获取参数;

  3. SQL模板格式:SQL模板中的参数需写成 #{params.xxx}

 7

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


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

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