MyBatis 添加元数据自定义元素标签的实现代码
开发背景
现有系统中维护了一套业务表相关列、键的元数据,希望通过读取元数据实现自动封装SQL语句、自定义主键策略。实现方案为入侵式修改MyBatis,增加元素标签meta,支持业务开发中可以在XML映射文件中使用。
meta元素设计如下:
期望示例如下:
select fromUSER where =#{__PK_VALUE}
开发准备
新建项目并引入mybatis、mybatis-spring两个核心依赖。
org.mybatis mybatis org.mybatis mybatis-spring
增加自定义元素
创建MetaHandler和MetaSqlNode
publicclassMetaHandlerimplementsNodeHandler{
privatefinalCustomConfigurationconfiguration;
protectedMetaHandler(CustomConfigurationconfiguration){
this.configuration=configuration;
}
@Override
publicvoidhandleNode(XNodenodeToHandle,ListtargetContents){
finalStringtest=nodeToHandle.getStringAttribute("test");
finalStringtype=nodeToHandle.getStringAttribute("type");
finalStringignore=nodeToHandle.getStringAttribute("ignore");
finalStringtable=nodeToHandle.getStringAttribute("table");
finalStringfunc=nodeToHandle.getStringAttribute("func");
Stringalias=nodeToHandle.getStringAttribute("alias");
if(!StringUtils.isEmpty(alias)){
alias=alias.trim();
//是否无效防止注入
booleaninvalid=alias.contains("")||alias.contains(".");
if(invalid){
thrownewRuntimeException("aliasisinvalid:"+alias);
}
}
MetaSqlNodemetaSqlNode=newMetaSqlNode(configuration,test,type,ignore,table,func,alias);
targetContents.add(metaSqlNode);
}
}
publicclassMetaSqlNodeimplementsSqlNode{
/**
*mybatis核心数据
*/
privatefinalCustomConfigurationconfiguration;
/**
*判断语句校验器
*/
privatefinalExpressionEvaluatorevaluator;
/**
*判断语句,同if标签
*/
privatefinalStringtest;
/**
*生成语句类型update|insert|select|columns|pk-col|load|load-columns
*/
privatefinalTypeEnumtype;
/**
*忽略的列
*/
privatefinalStringignore;
/**
*表名,未指定则从调用参数中获取
*/
privatefinalStringtable;
/**
*功能,未指定则从调用参数中获取
*/
privatefinalStringfunc;
/**
*动态列别名
*/
privatefinalStringalias;
publicMetaSqlNode(CustomConfigurationconfiguration,Stringtest,Stringtype,Stringignore,Stringtable,Stringfunc,Stringalias){
this.evaluator=newExpressionEvaluator();
this.configuration=configuration;
this.test=test;
this.type=TypeEnum.parse(type);
this.ignore=ignore;
this.table=table;
this.func=func;
this.alias=alias;
}
@Override
publicbooleanapply(DynamicContextcontext){
//TODO解析type与table,向context中添加语句
context.appendSql("insert······");
}
}
创建CustomXMLScriptBuilder
内容复制自org.apache.ibatis.scripting.xmltags.XMLScriptBuilder,在initNodeHandlerMap方法中添加MetaHandler。
privatevoidinitNodeHandlerMap(){
nodeHandlerMap.put("trim",newTrimHandler());
nodeHandlerMap.put("where",newWhereHandler());
nodeHandlerMap.put("set",newSetHandler());
nodeHandlerMap.put("foreach",newForEachHandler());
nodeHandlerMap.put("if",newIfHandler());
nodeHandlerMap.put("choose",newChooseHandler());
nodeHandlerMap.put("when",newIfHandler());
nodeHandlerMap.put("otherwise",newOtherwiseHandler());
nodeHandlerMap.put("bind",newBindHandler());
//增加元数据标签解析器
if(configurationinstanceofCustomConfiguration){
nodeHandlerMap.put("meta",newMetaHandler((CustomConfiguration)configuration));
}
}
创建CustomXMLLanguageDriver
内容复制自org.apache.ibatis.scripting.xmltags.XMLLanguageDriver,在createSqlSource方法中使用CustomXMLScriptBuilder来解析Xml生成SqlSource。
@Override
publicSqlSourcecreateSqlSource(Configurationconfiguration,XNodescript,Class>parameterType){
CustomXMLScriptBuilderbuilder=newCustomXMLScriptBuilder(configuration,script,parameterType);
returnbuilder.parseScriptNode();
}
创建CustomConfiguration
继承org.apache.ibatis.session.Configuration,内容复制自Configuration。将构造方法中的XMLLanguageDriver修改为CustomConfiguration。
publicCustomConfiguration(){
······
//默认使用自定义LanguageDriver
typeAliasRegistry.registerAlias("XML",CustomXMLLanguageDriver.class);
······
//默认使用自定义LanguageDriver
languageRegistry.setDefaultDriverClass(CustomXMLLanguageDriver.class);
······
}
创建CustomXMLConfigBuilder
内容复制自org.apache.ibatis.builder.xml.XMLConfigBuilder,支持通过XML配置来创建CustomConfiguration。
publicclassCustomXMLConfigBuilderextendsBaseBuilder{
······
privateCustomXMLConfigBuilder(XPathParserparser,Stringenvironment,Propertiesprops){
//使用CustomConfiguration
super(newCustomConfiguration());
ErrorContext.instance().resource("SQLMapperConfiguration");
this.configuration.setVariables(props);
this.parsed=false;
this.environment=environment;
this.parser=parser;
}
······
}
创建SqlSessionFactory
复制自org.mybatis.spring.SqlSessionFactoryBean,将buildSqlSessionFactory方法中的Configuration替换为CustomConfiguration。
protectedSqlSessionFactorybuildSqlSessionFactory()throwsException{
finalConfigurationtargetConfiguration;
CustomXMLConfigBuilderxmlConfigBuilder=null;
if(this.configuration!=null){
targetConfiguration=this.configuration;
if(targetConfiguration.getVariables()==null){
targetConfiguration.setVariables(this.configurationProperties);
}elseif(this.configurationProperties!=null){
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
}elseif(this.configLocation!=null){
//使用CustomXMLConfigBuilder创建CustomConfiguration
xmlConfigBuilder=newCustomXMLConfigBuilder(this.configLocation.getInputStream(),null,this.configurationProperties);
targetConfiguration=xmlConfigBuilder.getConfiguration();
}else{
LOGGER.debug(
()->"Property'configuration'or'configLocation'notspecified,usingdefaultMyBatisConfiguration");
//使用CustomConfiguration
targetConfiguration=newCustomConfiguration();
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
}
······
returnthis.sqlSessionFactoryBuilder.build(targetConfiguration);
}
修改DTD约束
MyBatis的约束文件并不支持自定义的meta元素,需要使用CDATA来处理。示例如下:
]]>
如果不想要CDATA,则需要通过修改DTD约束来完成。
- 在classes下指定位置添加DTD约束文件org/apache/ibatis/builder/xml/mybatis-3-config.dtd达到覆盖MyBatisDTD的效果。
- 重写代码来使用指定DTD。
创建CustomXMLMapperEntityResolver
复制自org.apache.ibatis.builder.xml.XMLMapperEntityResolver,将MYBATIS_MAPPER_DTD修改为指向本地mybatis-3-mapper.dtd文件,并在DTD文件中添加meta元素的约束。
publicclassCustomXMLMapperEntityResolverimplementsEntityResolver{
······
privatestaticfinalStringMYBATIS_MAPPER_DTD="com/my/ibatis/builder/xml/mybatis-3-mapper.dtd";
······
}
CustomXMLLanguageDriver
Mapper动态语句注解处理使用CustomXMLMapperEntityResolver。
/** *Mapper动态语句注解调用 **"" * *@paramconfigurationmybatis配置 *@paramscript动态语句字符串 *@paramparameterType参数类型 *@returnorg.apache.ibatis.mapping.SqlSource */ @Override publicSqlSourcecreateSqlSource(Configurationconfiguration,Stringscript,Class>parameterType){ //issue#3 if(script.startsWith("