SpringBoot像Mybatis-Plus一样动态加载Mapper文件,实现Mapper文件动态更新

摘要:本文主要介绍在SpringBoot环境中动态加载MybatisMapper.xml文件内容,为方便我们在前端实现自定义的业务,让业务更加灵活。

背景

在业务报表系统中,我们经常需要查询各种业务数据,我们需要前端自己写SQL语句来拼接查询数据,所以需要实现一个安全动态执行SQL的一个接口,并且SQL最好和业务系统的mybatis写法保持一致,所以有了这个业务。

基于SpringBoot的案例

pom.xml

xml 复制代码
<artifactId>springboot-dymapper</artifactId>

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.7</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.75</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>4.4.4</version>
    </dependency>
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
    </dependency>
</dependencies>

DymapperApplication启动类

less 复制代码
@MapperScan("com.huzhihui.dymapper.mapper")
@SpringBootApplication
public class DymapperApplication {

    public static void main(String[] args) {
        SpringApplication.run(DymapperApplication.class, args);
    }

}

application.yml配置类

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://192.168.18.111:30036/zdy-01?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&useAffectedRows=true&allowMultiQueries=true
    username: xxx
    password: xxx
    hikari:
      maximumPoolSize: 500
      minimumIdle: 500
      idleTimeout: 11000
      connectionTimeout: 30000
      maxLifetime: 31000

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

基础数据准备相关mapper和数据库表

准备数据库表

  • dynamic_mapper:用来存储自定义的mapper配置
  • sys_user:业务测试用
sql 复制代码
CREATE TABLE `dynamic_mapper` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `mapper_id` varchar(100) DEFAULT NULL COMMENT '映射ID',
  `description` varchar(200) DEFAULT NULL COMMENT '接口描述',
  `xml_content` text DEFAULT NULL COMMENT 'XML配置内容',
  `status` tinyint(4) DEFAULT 1 COMMENT '状态(0:禁用,1:启用)',
  `create_time` datetime DEFAULT current_timestamp(),
  `update_time` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
  PRIMARY KEY (`id`),
  UNIQUE KEY `mapper_id` (`mapper_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1872569560585469954 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

DynamicMapper实体

typescript 复制代码
@Data
public class DynamicMapper {

    private Long id;

    private String mapperId;

    private String description;

    private String xmlContent;

}

DynamicMapperMapper mapper接口

java 复制代码
@Mapper
public interface DynamicMapperMapper extends BaseMapper<DynamicMapper> {

}

动态mapper核心代码

DynamicMapperTemplateHandler模版内容加载类

用于动态生成mapper.xml内容给mybatis加载

typescript 复制代码
@Component
public class DynamicMapperTemplateHandler {

    public String generateContent(String mapperId, String mapperContent){
        try{
            Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
            cfg.setClassForTemplateLoading(this.getClass(), "/template/");
            Template template = cfg.getTemplate("dynamicMapperTemplate.ftl");
            StringWriter out = new StringWriter();
            Map<String,Object> dataModel = new HashMap<>();
            dataModel.put("mapperId", mapperId);
            dataModel.put("mapperContent", mapperContent);
            template.process(dataModel, out);
            return out.toString();
        }catch (Exception ex){
            throw new RuntimeException(ex);
        }
    }

}

DynamicMapperLoader

记载mapper.xml文件内容到sqlSessionFactory

scss 复制代码
@Service
@Slf4j
public class DynamicMapperLoader implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Autowired
    private DynamicMapperMapper mapperMapper;

    private Set<String> loadedResources;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 通过反射获取 loadedResources
        try {
            Field loadedResourcesField = Configuration.class.getDeclaredField("loadedResources");
            loadedResourcesField.setAccessible(true);
            this.loadedResources = (Set<String>) loadedResourcesField.get(sqlSessionFactory.getConfiguration());
        } catch (Exception e) {
            log.error("Failed to get loadedResources field", e);
            throw new RuntimeException(e);
        }
        loadDynamicMappers();
    }

    public void loadDynamicMappers() {
        Configuration configuration = sqlSessionFactory.getConfiguration();
        List<DynamicMapper> mappers = mapperMapper.selectList(Wrappers.lambdaQuery());

        for (DynamicMapper mapper : mappers) {
            try {
                removeMapper(mapper.getMapperId(), configuration);

                String xmlContent = buildXmlContent(mapper);
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(
                        new ByteArrayInputStream(xmlContent.getBytes()),
                        configuration,
                        mapper.getMapperId(),
                        configuration.getSqlFragments()
                );
                xmlMapperBuilder.parse();
                log.info("加载动态Mapper成功: {}", mapper.getMapperId());
            } catch (Exception e) {
                log.error("加载动态Mapper失败: {}", mapper.getMapperId(), e);
            }
        }
    }

    /**
     * 删除Mapper
     */
    private void removeMapper(String mapperId, Configuration configuration) {
        String namespace = "dynamic." + mapperId;
        loadedResources.remove(mapperId);

        // 清除 MappedStatements
        List<MappedStatement> matches = configuration.getMappedStatements().stream().filter(v -> v.getId().startsWith(namespace)).collect(Collectors.toList());
        configuration.getMappedStatements().removeAll(matches);

        // 通过反射获取 loadedResources
        try {
            Field loadedResourcesField = Configuration.class.getDeclaredField("loadedResources");
            loadedResourcesField.setAccessible(true);
            this.loadedResources = (Set<String>) loadedResourcesField.get(configuration);
        } catch (Exception e) {
            log.error("Failed to get loadedResources field", e);
            this.loadedResources = new HashSet<>();
        }

        // 清除 ResultMaps
        configuration.getResultMapNames().stream()
                .filter(resultMapId -> resultMapId.startsWith(namespace))
                .forEach(resultMapId -> configuration.getResultMaps().remove(resultMapId));

        // 清除 Caches
        configuration.getCaches().clear();

        // 清除 SqlFragments
        new ArrayList<>(configuration.getSqlFragments().keySet()).stream()
                .filter(fragment -> fragment.startsWith(namespace))
                .forEach(fragment -> configuration.getSqlFragments().remove(fragment));
    }

    private String buildXmlContent(DynamicMapper mapper) {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" " +
                "\"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n" +
                "<mapper namespace=\"dynamic." + mapper.getMapperId() + "\">\n" +
                mapper.getXmlContent() + "\n" +
                "</mapper>";
    }

}

DynamicSqlExecutor执行入口

typescript 复制代码
@Service
@Slf4j
public class DynamicSqlExecutor {

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    public Object execute(String mapperId, String methodId, Object params) {
        String fullMethodId = "dynamic." + mapperId + "." + methodId;
        try {
            MappedStatement mappedStatement = sqlSessionTemplate.getConfiguration()
                    .getMappedStatement(fullMethodId);

            // 根据SQL类型执行不同操作
            SqlCommandType sqlType = mappedStatement.getSqlCommandType();
            switch (sqlType) {
                case SELECT:
                    // 判断是否返回列表
                    return sqlSessionTemplate.selectList(fullMethodId, params);
//                    if (mappedStatement.getResultMaps().get(0).getType().equals(List.class)) {
//                        return sqlSessionTemplate.selectList(fullMethodId, params);
//                    } else {
//                        return sqlSessionTemplate.selectOne(fullMethodId, params);
//                    }
                case INSERT:
                    return sqlSessionTemplate.insert(fullMethodId, params);
                case UPDATE:
                    return sqlSessionTemplate.update(fullMethodId, params);
                case DELETE:
                    return sqlSessionTemplate.delete(fullMethodId, params);
                default:
                    throw new UnsupportedOperationException("不支持的SQL类型");
            }
        } catch (Exception e) {
            log.error("执行动态SQL失败: {}", fullMethodId, e);
            throw new RuntimeException("SQL执行失败", e);
        }
    }
}

入口Controller

typescript 复制代码
@RestController
@RequestMapping("/api/mapper")
public class DynamicMapperController {

    @Autowired
    private DynamicMapperMapper mapperMapper;

    @Autowired
    private DynamicMapperLoader mapperLoader;

    @Autowired
    private DynamicSqlExecutor sqlExecutor;

    @PostMapping("/add")
    public Object addMapper(@RequestBody DynamicMapper mapper) {
        // 验证XML内容
        validateXmlContent(mapper.getXmlContent());

        // 保存Mapper配置
        mapperMapper.insert(mapper);

        // 重新加载Mapper
        mapperLoader.loadDynamicMappers();

        return "success";
    }

    @PostMapping("/execute")
    public Object execute(@RequestParam String mapperId,
                             @RequestParam String methodId,
                             @RequestBody Map<String, Object> params) {
        Object result = sqlExecutor.execute(mapperId, methodId, params);
        return result;
    }

    private void validateXmlContent(String xmlContent) {
        try {
            // 验证XML格式
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.parse(new ByteArrayInputStream(xmlContent.getBytes()));

            // 验证是否包含危险操作
            String upperXml = xmlContent.toUpperCase();
            if (upperXml.contains("DELETE") || upperXml.contains("DROP") ||
                    upperXml.contains("TRUNCATE")) {
                throw new Exception("XML内容包含危险操作");
            }
        } catch (Exception e) {
            throw new RuntimeException("XML格式验证失败");
        }
    }

    @Autowired
    private DynamicMapperTemplateHandler dynamicMapperTemplateHandler;

    @GetMapping("buildTemplateStr")
    public Object buildTemplateStr(){
        String content = "<select id="getUserList" parameterType="map" resultType="map">\n" +
                "    SELECT * FROM sys_user\n" +
                "    <where>\n" +
                "        <if test="username != null and username != ''">\n" +
                "            AND username LIKE CONCAT('%', #{username}, '%')\n" +
                "        </if>\n" +
                "        <if test="status != null">\n" +
                "            AND status = #{status}\n" +
                "        </if>\n" +
                "        <if test="deptIds != null">\n" +
                "            AND dept_id IN\n" +
                "            <foreach collection="deptIds" item="deptId" open="(" separator="," close=")">\n" +
                "                #{deptId}\n" +
                "            </foreach>\n" +
                "        </if>\n" +
                "    </where>\n" +
                "    ORDER BY create_time DESC\n" +
                "</select>";
        String s = dynamicMapperTemplateHandler.generateContent("abc", content);
        System.out.println(s);
        return s;
    }
}

测试

新增一个mapper方法

更新已经存在的mapper方法

执行

相关推荐
AirMan39 分钟前
深入解析 Spring Caffeine:揭秘 W-TinyLFU 缓存淘汰策略的高命中率秘密
后端
小码编匠1 小时前
C# Bitmap 类在工控实时图像处理中的高效应用与避坑
后端·c#·.net
布朗克1681 小时前
Spring Boot项目通过RestTemplate调用三方接口详细教程
java·spring boot·后端·resttemplate
uhakadotcom2 小时前
使用postgresql时有哪些简单有用的最佳实践
后端·面试·github
IT毕设实战小研3 小时前
基于Spring Boot校园二手交易平台系统设计与实现 二手交易系统 交易平台小程序
java·数据库·vue.js·spring boot·后端·小程序·课程设计
bobz9653 小时前
QT 字体
后端
泉城老铁3 小时前
Spring Boot 中根据 Word 模板导出包含表格、图表等复杂格式的文档
java·后端
用户4099322502123 小时前
如何在FastAPI中玩转APScheduler,实现动态定时任务的魔法?
后端·github·trae
孤狼程序员3 小时前
【Spring Cloud 微服务】1.Hystrix断路器
java·spring boot·spring·微服务
风象南3 小时前
开发者必备工具:用 SpringBoot 构建轻量级日志查看器,省时又省力
后端