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方法

执行

相关推荐
Asthenia04125 分钟前
Linux系统的页表一般多大?内存不足时强行申请内存会如何?
后端
小宝潜行6 分钟前
SpringBoot之核心特性理解和Jar启动命令运行原理
spring boot·后端·jar
Asthenia041216 分钟前
JavaSE:进程/线程/协程!你真的明白了么?
后端
猿来入此小猿29 分钟前
基于SpringBoot+Vue3实现的宠物领养管理平台功能一
java·spring boot·毕业设计·宠物·宠物领养·宠物平台·免费学习
gongzemin1 小时前
Mac 安装MongoDB 社区版
后端·mongodb
宇瞳月1 小时前
Rust语言的嵌入式Linux
开发语言·后端·golang
Java中文社群1 小时前
拿下美团实习~
java·后端·面试
用户86178277365181 小时前
整表复制
java·后端·mysql
lovebugs1 小时前
CAS是什么?AtomicInteger如何利用它?ABA问题如何解决?
后端·面试
小巫编程室1 小时前
快速入门-Java Lambda
java·后端·面试