现有系统中维护了一套业务表相关列、键的元数据,希望通过读取元数据实现自动封装 SQL 语句、自定义主键策略。实现方案为入侵式修改 MyBatis,增加元素标签 meta ,支持业务开发中可以在XML映射文件中使用。
meta元素设计如下:
<!-- meta标签 可根据参数获取到对应的表名 动态生成语句 --> <!ELEMENT meta EMPTY> <!ATTLIST meta test CDATA #IMPLIED type (update|insert|select|columns|pk-col|load|load-columns) #IMPLIED ignore CDATA #IMPLIED table CDATA #IMPLIED func CDATA #IMPLIED alias CDATA #IMPLIED > 复制代码
期望示例如下:
<insert id="insertMap" useGeneratedKeys="true" generator="meta"> <meta table="USER" type="insert"/> </insert> <update id="updateMap"> <meta table="USER" type="update"/> </update> <select id="selectOneByPk" resultType="java.util.HashMap"> select <meta table="USER" type="columns"/> from USER where <meta table="USER" type="pk-col"/> = #{__PK_VALUE} </select> 复制代码
新建项目并引入 mybatis 、 mybatis-spring 两个核心依赖。
<!-- mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> </dependency> <!-- mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> </dependency> 复制代码
public class MetaHandler implements NodeHandler { private final CustomConfiguration configuration; protected MetaHandler(CustomConfiguration configuration) { this.configuration = configuration; } @Override public void handleNode(XNode nodeToHandle, List<SqlNode> 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) { throw new RuntimeException("alias is invalid : " + alias); } } MetaSqlNode metaSqlNode = new MetaSqlNode(configuration, test, type, ignore, table, func, alias); targetContents.add(metaSqlNode); } } 复制代码
public class MetaSqlNode implements SqlNode { /** * mybatis核心数据 */ private final CustomConfiguration configuration; /** * 判断语句校验器 */ private final ExpressionEvaluator evaluator; /** * 判断语句,同if标签 */ private final String test; /** * 生成语句类型 update|insert|select|columns|pk-col|load|load-columns */ 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 = new 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 ······ "); } } 复制代码
内容复制自 org.apache.ibatis.scripting.xmltags.XMLScriptBuilder ,在 initNodeHandlerMap 方法中添加 MetaHandler。
private void initNodeHandlerMap() { nodeHandlerMap.put("trim", new TrimHandler()); nodeHandlerMap.put("where", new WhereHandler()); nodeHandlerMap.put("set", new SetHandler()); nodeHandlerMap.put("foreach", new ForEachHandler()); nodeHandlerMap.put("if", new IfHandler()); nodeHandlerMap.put("choose", new ChooseHandler()); nodeHandlerMap.put("when", new IfHandler()); nodeHandlerMap.put("otherwise", new OtherwiseHandler()); nodeHandlerMap.put("bind", new BindHandler()); //增加元数据标签解析器 if (configuration instanceof CustomConfiguration) { nodeHandlerMap.put("meta", new MetaHandler((CustomConfiguration) configuration)); } } 复制代码
内容复制自 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver ,在 createSqlSource 方法中使用 CustomXMLScriptBuilder 来解析Xml生成 SqlSource。
@Override public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) { CustomXMLScriptBuilder builder = new CustomXMLScriptBuilder(configuration, script, parameterType); return builder.parseScriptNode(); } 复制代码
继承 org.apache.ibatis.session.Configuration ,内容复制自 Configuration。将构造方法中的 XMLLanguageDriver 修改为 CustomConfiguration。
public CustomConfiguration() { ······ //默认使用自定义 LanguageDriver typeAliasRegistry.registerAlias("XML", CustomXMLLanguageDriver.class); ······ //默认使用自定义 LanguageDriver languageRegistry.setDefaultDriverClass(CustomXMLLanguageDriver.class); ······ } 复制代码
内容复制自 org.apache.ibatis.builder.xml.XMLConfigBuilder ,支持通过 XML 配置来创建 CustomConfiguration。
public class CustomXMLConfigBuilder extends BaseBuilder { ······ private CustomXMLConfigBuilder(XPathParser parser, String environment, Properties props) { // 使用 CustomConfiguration super(new CustomConfiguration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } ······ } 复制代码
复制自 org.mybatis.spring.SqlSessionFactoryBean ,将 buildSqlSessionFactory 方法中的 Configuration 替换为 CustomConfiguration。
protected SqlSessionFactory buildSqlSessionFactory() throws 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 = new 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 = new CustomConfiguration(); Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables); } ······ return this.sqlSessionFactoryBuilder.build(targetConfiguration); } 复制代码
MyBatis 的约束文件并不支持自定义的 meta 元素,需要使用 CDATA 来处理。示例如下:
<insert id="insertMap" useGeneratedKeys="true" generator="meta"> <![CDATA[[ <meta table="USER" type="insert"/> ]]> </insert> 复制代码
如果不想要 CDATA,则需要通过修改DTD约束来完成。
复制自 org.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 = "com/my/ibatis/builder/xml/mybatis-3-mapper.dtd"; ······ } 复制代码
<!-- meta标签 可根据参数获取到对应的表名 动态生成语句 --> <!ELEMENT meta EMPTY> <!ATTLIST meta test CDATA #IMPLIED type (update|insert|select|columns|pk-col|load|load-columns) #IMPLIED ignore CDATA #IMPLIED table CDATA #IMPLIED func CDATA #IMPLIED alias CDATA #IMPLIED > 复制代码
Mapper动态语句注解处理使用 CustomXMLMapperEntityResolver。
/** * Mapper动态语句注解调用 * <p> * "<script>select * from user <if test=/"id !=null /">where id = #{id} </if></script>" * * @param configuration mybatis配置 * @param script 动态语句字符串 * @param parameterType 参数类型 * @return org.apache.ibatis.mapping.SqlSource */ @Override public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { // issue #3 if (script.startsWith("<script>")) { //将动态语句字符串转换为XNode对象 XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new CustomXMLMapperEntityResolver()); return createSqlSource(configuration, parser.evalNode("/script"), parameterType); } else { // issue #127 script = PropertyParser.parse(script, configuration.getVariables()); TextSqlNode textSqlNode = new TextSqlNode(script); if (textSqlNode.isDynamic()) { return new CustomDynamicSqlSource(configuration, textSqlNode); } else { return new RawSqlSource(configuration, script, parameterType); } } } 复制代码
复制自 org.apache.ibatis.builder.xml.XMLMapperBuilder ,修改构造函数使用 CustomXMLMapperEntityResolver 解析xml。
public CustomXMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { this(new XPathParser(inputStream, true, configuration.getVariables(), new CustomXMLMapperEntityResolver()), configuration, resource, sqlFragments); } 复制代码
修改 buildSqlSessionFactory 方法,使用 CustomXMLMapperBuilder 来解析xml。
protected SqlSessionFactory buildSqlSessionFactory() throws Exception { ······ try { //使用自定义 XMLMapperBuilder CustomXMLMapperBuilder xmlMapperBuilder = new CustomXMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } ······ } 复制代码
复制自 org.apache.ibatis.builder.annotation.MapperAnnotationBuilder ,修改 loadXmlResource 方法使用 CustomXMLMapperBuilder。
private void loadXmlResource() { if (!configuration.isResourceLoaded("namespace:" + type.getName())) { ······ if (inputStream != null) { //使用自定义解析器支持自定义标签 CustomXMLMapperBuilder xmlParser = new CustomXMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); xmlParser.parse(); } } } 复制代码
复制自 org.apache.ibatis.binding.MapperRegistry ,修改 addMapper 方法使用 CustomMapperAnnotationBuilder。
@Override public <T> void addMapper(Class<T> type) { if (type.isInterface()) { ······ try { knownMappers.put(type, new MapperProxyFactory<>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. CustomMapperAnnotationBuilder parser = new CustomMapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } } 复制代码
修改 mapperRegistry 属性使用 CustomMapperRegistry。
public class CustomConfiguration extends Configuration { ······ protected final MapperRegistry mapperRegistry = new CustomMapperRegistry(this); ······ } 复制代码
<!-- Mybatis SessionFactory--> <bean id="sqlSessionFactory" class="com.my.ibatis.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configurationProperties" > <bean class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="locations" value="classpath*:mybatis.properties"/> </bean> </property> </bean> 复制代码
@Configuration public class MybatisConfig { @Bean public PropertiesFactoryBean createPropertiesFactoryBean() throws IOException { PropertiesFactoryBean bean = new PropertiesFactoryBean(); bean.setLocation(new ClassPathResource("mybatis.properties")); return bean; } @Bean("sqlSessionFactory") public SqlSessionFactoryBean createSqlSessionFactory(DataSource dataSource, PropertiesFactoryBean bean) throws IOException { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setConfigurationProperties(bean.getObject()); return factoryBean; } } 复制代码
很遗憾的说,推酷将在这个月底关闭。人生海海,几度秋凉,感谢那些有你的时光。
原文 https://juejin.im/post/5f1938f76fb9a07e8e45075a