摘要:本文主要介绍在SpringBoot
环境中动态加载Mybatis
的Mapper.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;
}
}