MyBatis 添加元数据自定义元素标签的实现代码
开发背景
现有系统中维护了一套业务表相关列、键的元数据,电脑维修网希望通过读取元数据实现自动封装 SQL 语句、自定义主键策略。实现方案为入侵式修改 MyBatis,增加元素标签meta,支持业务开发中可以在XML映射文件中使用。
meta元素设计如下
期望示例如下
开发准备
新建项目并引入 mybatis 、 mybatis-spring 两个核心依赖。
.mybatis mybatis.mybatis mybatis-spring
增加自定义元素
创建 metaHandler 和 metaSqlNode
public class metaHandler implements NodeHandler {
private final CustomConfiguration configuration;
protected metaHandler(CustomConfiguration configuration) {
this.configuration = configuration;
}
@Override
public void handleNode(XNode nodeToHandle, List targetContents) {
final String test = nodeToHandle.getStringAttribute("test");
final String type = nodeToHandle.getStringAttribute("type");
final String ignore = nodeToHandle.getStringAttribute("ignore");
final String table = nodeToHandle.getStringAttribute("table");
final String func = nodeToHandle.getStringAttribute("func");
String alias = nodeToHandle.getStringAttribute("alias");
if (!StringUtils.isEmpty(alias)) {
alias = alias.trim();
//是否无效 防止注入
boolean invalid = alias.contains(" ") || alias.contains(".");
if (invalid) {
thro ne RuntimeException("alias is invalid : " + alias);
}
}
metaSqlNode metaSqlNode = ne metaSqlNode(configuration, test, type, ignore, table, func, alias);
targetContents.add(metaSqlNode);
}
}
public class metaSqlNode implements SqlNode {
private final CustomConfiguration configuration;
private final expressionevaluator evaluator;
private final String test;
private final TypeEnum type;
private final String ignore;
private final String table;
private final String func;
private final String alias;
public metaSqlNode(CustomConfiguration configuration, String test, String type, String ignore, String table, String func, String alias) {
this.evaluator = ne expressionevaluator();
this.configuration = configuration;
this.test = test;
this.type = TypeEnum.parse(type);
this.ignore = ignore;
this.table = table;
this.func = func;
this.alias = alias;
}
@Override
public boolean apply(DynamicContext context) {
// TODO 解析type与table,向context中添加语句
context.appendSql(" insert ······ ");
}
}
创建 CustomXMLscriptBuilder
内容复制自.apache.ibatis.scripting.xmltags.XMLscriptBuilder,在 initNodeHandlerMap 方法中添加 metaHandler。
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", ne TrimHandler());
nodeHandlerMap.put("here", ne WhereHandler());
nodeHandlerMap.put("set", ne SetHandler());
nodeHandlerMap.put("foreach", ne ForEachHandler());
nodeHandlerMap.put("if", ne IfHandler());
nodeHandlerMap.put("choose", ne ChooseHandler());
nodeHandlerMap.put("hen", ne IfHandler());
nodeHandlerMap.put("otherise", ne OtheriseHandler());
nodeHandlerMap.put("bind", ne BindHandler());
//增加元数据标签解析器
if (configuration instanceof CustomConfiguration) {
nodeHandlerMap.put("meta", ne metaHandler((CustomConfiguration) configuration));
}
}
创建 CustomXMLLanguageDriver
内容复制自.apache.ibatis.scripting.xmltags.XMLLanguageDriver,在 createSqlSource 方法中使用 CustomXMLscriptBuilder 来解析Xml生成 SqlSource。
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class> parameterType) {
CustomXMLscriptBuilder builder = ne CustomXMLscriptBuilder(configuration, script, parameterType);
return builder.parsescriptNode();
}
创建 CustomConfiguration
继承.apache.ibatis.session.Configuration,内容复制自 Configuration。将构造方法中的 XMLLanguageDriver 修改为 CustomConfiguration。
public CustomConfiguration() {
······
//默认使用自定义 LanguageDriver
typeAliasRegistry.registerAlias("XML", CustomXMLLanguageDriver.class);
······
//默认使用自定义 LanguageDriver
languageRegistry.setDefaultDriverClass(CustomXMLLanguageDriver.class);
······
}
创建 CustomXMLConfigBuilder
内容复制自.apache.ibatis.builder.xml.XMLConfigBuilder,支持通过 XML 配置来创建 CustomConfiguration。
public class CustomXMLConfigBuilder extends baseBuilder {
······
private CustomXMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 使用 CustomConfiguration
super(ne CustomConfiguration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
······
}
创建 SqlSessionFactory
复制自.mybatis.spring.SqlSessionFactoryBean,将 buildSqlSessionFactory 方法中的 Configuration 替换为 CustomConfiguration。
protected SqlSessionFactory buildSqlSessionFactory() thros Exception {
final Configuration targetConfiguration;
CustomXMLConfigBuilder xmlConfigBuilder = null;
if (this.configuration != null) {
targetConfiguration = this.configuration;
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
// 使用 CustomXMLConfigBuilder 创建 CustomConfiguration
xmlConfigBuilder = ne CustomXMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
targetConfiguration = xmlConfigBuilder.getConfiguration();
} else {
LOGGER.debug(
() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
// 使用 CustomConfiguration
targetConfiguration = ne CustomConfiguration();
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
}
······
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
修改DTD约束
MyBatis 的约束文件并不支持自定义的 meta 元素,需要使用 CDATA 来处理。示例如下
]]>
如果不想要 CDATA,则需要通过修改DTD约束来完成。
- 在 classes 下指定位置添加DTD约束文件/apache/ibatis/builder/xml/mybatis-3-config.dtd达到覆盖 MyBatis DTD的效果。
- 重写代码来使用指定 DTD 。
创建 CustomXMLMapperEntityResolver
复制自.apache.ibatis.builder.xml.XMLMapperEntityResolver,将MYBATIS_MAPPER_DTD修改为指向本地 mybatis-3-mapper.dtd 文件,并在DTD文件中添加 meta 元素的约束。
public class CustomXMLMapperEntityResolver implements EntityResolver {
······
private static final String MYBATIS_MAPPER_DTD = "/my/ibatis/builder/xml/mybatis-3-mapper.dtd";
······
}
CustomXMLLanguageDriver
Mapper动态语句注解处理使用 CustomXMLMapperEntityResolver。
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class> parameterType) {
// issue #3
if (script.startsWith("