1.前言
这一章主要的就是要解析动态标签里的Sql语句,然后进行条件语句的拼接,动态标签实现了trim和if标签,所以Sql节点就要加上TrimSqlNode和ifSqlNode,我们最终要获取Sql源,动态Sql语句需要一些处理,所以需要添加DynamicSqlSource来处理动态Sql语句的调用和一些业务逻辑处理。
本章节主要就是要处理如下图片的解析Sql内容,把如下图片的Sql内容更改为能够可执行的Sql语句,这个是目标。
需要注意的是,sql语句不加条件时我们叫静态SQL,当动态语句标签包含的条件语句时,除了trim和if放入到对应的节点里,if里的Sql也要放入静态节点里,最后把这些个节点集合放入到混合节点里,等使用时直接遍历混合节点数据即可,最终调度到不同的节点取出Sql进行拼接即可。
2.xml类图
动态标签我们看作是一个节点,那么我们解析的xml的Sql语句里边就有很多的不同的节点,静态的Sql节点,if节点,trim节点,那么需要把不同节点的信息存储到不同的节点里,这块的功能需要在xml脚本构建类里实现,从这里开始去一点一点构建不同的节点。
节点构建完毕需要获取不同的文本进行SQL语句拼接,拼接完毕放入到DynamicContext的sqlBuilder里, DynamicSqlSource就可以根据DynamicContext直接获取到SQL了。
3.代码
因为我们要处理的是Sql里的内容,所以在代码设计里就是要处理Xml的脚本构建,也就是XMLScriptBuilder类,我们在XMLScriptBuilder类里添加了NodeHandler接口,定义了handleNode方法。
3.1 节点处理器(NodeHandler)
包名路径:package cn.bugstack.mybatis.scripting.xmltags;
然后定义两个实现类,TrimHandler和IfHandler类,TrimHandler主要解析trim标签,IfHandler主要处理if标签内容。
最后再初始化nodeHandler把TrimHandler和IfHandler放入到Map里,留着后面从Map取出使用。
public class XMLScriptBuilder extends BaseBuilder {// 过滤其他public XMLScriptBuilder(Configuration configuration, Element element, Class<?> parameterType) {super(configuration);this.element = element;this.parameterType = parameterType;initNodeHandlerMap();}// step-15新增private void initNodeHandlerMap() {// 9种,实现其中2种 trim/where/set/foreach/if/choose/when/otherwise/bindnodeHandlerMap.put("trim", new TrimHandler());nodeHandlerMap.put("if", new IfHandler());}// 节点处理器private interface NodeHandler {void handleNode(Element nodeToHandle, List<SqlNode> targetContents);}/*** <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">...</trim> 解析 trim 标签信息,把字段 prefix、* prefixOverrides、suffixOverrides 都依次获取出来,使用 TrimSqlNode 构建后存放到 List<SqlNode> 中。* 得到trim的属性*/// step-15新增private class TrimHandler implements NodeHandler {@Overridepublic void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {List<SqlNode> contents = parseDynamicTags(nodeToHandle);MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);String prefix = nodeToHandle.attributeValue("prefix");String prefixOverrides = nodeToHandle.attributeValue("prefixOverrides");String suffix = nodeToHandle.attributeValue("suffix");String suffixOverrides = nodeToHandle.attributeValue("suffixOverrides");TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);targetContents.add(trim);}}/*** <if test="null != activityId">...</if> 解析if语句标签,与解析 trim 标签类似,* 获取标签配置 test 语句表达式,使用 IfSqlNode 进行构建,构建后存放到 List<SqlNode> 中。* 得到if标签的属性*/// step-15新增private class IfHandler implements NodeHandler {@Overridepublic void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {List<SqlNode> contents = parseDynamicTags(nodeToHandle);MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);// 得到test的判断语句String test = nodeToHandle.attributeValue("test");IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);targetContents.add(ifSqlNode);}}}
3.2 SqlNode
我们原来的SqlNode有静态的和混合的实现类,这次我们还要加三个实现类,TextSqlNode和IfSqlNode以及TrimSqlNode。
3.2.1 TextSqlNode
TextSqlNode:此节点处理是否是动态Sql的判断,还有一个是${}的参数替换。
/*** @Author df* @Description: 文本SQL节点(CDATA | TEXT)* @Date 2023/12/22 14:09*/
// step-15新增
public class TextSqlNode implements SqlNode {private String text;private Pattern injectionFilter;public TextSqlNode(String text) {this(text, null);}public TextSqlNode(String text, Pattern injectionFilter) {this.text = text;this.injectionFilter = injectionFilter;}/*** 判断是否是动态sql*/public boolean isDynamic() {DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();GenericTokenParser parser = createParser(checker);parser.parse(text);return checker.isDynamic();}@Overridepublic boolean apply(DynamicContext context) {GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));context.appendSql(parser.parse(text));return true;}// 处理${}替换值的情况private GenericTokenParser createParser(TokenHandler handler) {return new GenericTokenParser("${", "}", handler);}private static class BindingTokenParser implements TokenHandler {private DynamicContext context;private Pattern injectionFilter;public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {this.context = context;this.injectionFilter = injectionFilter;}@Overridepublic String handleToken(String content) {Object parameter = context.getBindings();if (parameter == null) {context.getBindings().put("value", null);} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {context.getBindings().put("value", parameter);}// 从缓存里取得值Object value = OgnlCache.getValue(content, context.getBindings());String srtValue = (value == null ? "" : String.valueOf(value));checkInjection(srtValue);return srtValue;}// 检查是否匹配正则表达式private void checkInjection(String value) {if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {throw new RuntimeException("Invalid input. Please conform to regex" + injectionFilter.pattern());}}}/*** 动态SQL检查器*/private static class DynamicCheckerTokenParser implements TokenHandler {private boolean isDynamic;public DynamicCheckerTokenParser() {// Prevent Synthetic Access}public boolean isDynamic() {return isDynamic;}@Overridepublic String handleToken(String content) {// 设置 isDynamic 为 true,即调用了这个类就必定是动态 SQLthis.isDynamic = true;return null;}}
}
3.2.2 IfSqlNode
IfSqlNode:它专门就是处理test的内容判断的,如果满足判断则进入拼接Sql语句
/*** @Author df* @Description: IF SQL 节点* @Date 2023/12/22 15:17*/
// step-15新增
public class IfSqlNode implements SqlNode {private ExpressionEvaluator evaluator;private String test;private SqlNode contents;public IfSqlNode(SqlNode contents, String test) {this.test = test;this.contents = contents;this.evaluator = new ExpressionEvaluator();}/*** <if test="null != activityId">* activity_id = #{activityId}* </if>*/@Overridepublic boolean apply(DynamicContext context) {// 如果满足条件,则apply,并返回trueif (evaluator.evaluateBoolean(test, context.getBindings())) {// 拼接if标签里的Sql语句contents.apply(context);return true;}return false;}
}
ExpressionEvaluator类:处理if的test内容判断的
public class ExpressionEvaluator {// 表达式求布尔值,比如 username == 'xiaofuge'public boolean evaluateBoolean(String expression, Object parameterObject) {// 非常简单,就是调用ognlObject value = OgnlCache.getValue(expression, parameterObject);if (value instanceof Boolean) {// 如果是Booleanreturn (Boolean) value;}if (value instanceof Number) {// 如果是Number,判断不为0return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);}// 否则判断不为nullreturn value != null;}
}
OgnlCache:OGNL缓存,处理if的表达式判断,并把表达式存储起来。
/*** @Author df* @Description: OGNL缓存:http://code.google.com/p/mybatis/issues/detail?id=342* OGNL 是 Object-Graph Navigation Language 的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL)* 通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。* 它使用相同的表达式去存取对象的属性。* @Date 2023/12/22 14:45*/
// step-15新增
public class OgnlCache {private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();private OgnlCache() {// Prevent Instantiation of Static Class}public static Object getValue(String expression, Object root) {try {Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());return Ognl.getValue(parseExpression(expression), context, root);} catch (OgnlException e) {throw new RuntimeException("Error evaluating expression '" + expression + "'. Cause: " + e, e);}}private static Object parseExpression(String expression) throws OgnlException {Object node = expressionCache.get(expression);if (node == null) {node = Ognl.parseExpression(expression);expressionCache.put(expression, node);}return node;}
}
OgnlClassResolver: 自己实现个OgnlClassResolver类加载器
public class OgnlClassResolver implements ClassResolver {private Map<String, Class<?>> classes = new HashMap<String, Class<?>>(101);@Overridepublic Class classForName(String className, Map map) throws ClassNotFoundException {Class<?> result = null;if ((result = classes.get(className)) == null) {try {result = Resources.classForName(className);} catch (ClassNotFoundException e1) {if (className.indexOf('.') == -1) {result = Resources.classForName("java.lang." + className);classes.put("java.lang." + className, result);}}classes.put(className, result);}return result;}
}
3.2.3 TrimSqlNode
TrimSqlNode:trim最主要的就是调用处理if的sql节点处理或拼接,然后得到trim属性,把trim属性前缀或后缀进行拼接Sql处理。
这里的trim的Sql拼接处理都在其内部类实现,是FilteredDynamicContext类。
/*** @Author df* @Description: trim Sql Node 节点解析* @Date 2023/12/22 14:57*/
// step-15新增
public class TrimSqlNode implements SqlNode {private SqlNode contents;private String prefix;private String suffix;private List<String> prefixesToOverride;private List<String> suffixesToOverride;private Configuration configuration;public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));}protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {this.contents = contents;this.prefix = prefix;this.prefixesToOverride = prefixesToOverride;this.suffix = suffix;this.suffixesToOverride = suffixesToOverride;this.configuration = configuration;}@Overridepublic boolean apply(DynamicContext context) {FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);// 得到trim里的内容,进行处理,最后拼接语句// 例如:trim->if->条件语句boolean result = contents.apply(filteredDynamicContext);// 根据trim的属性添加前后缀filteredDynamicContext.applyAll();return result;}/*** <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">** </trim>* 将prefixOverrides以list形式展示*/private static List<String> parseOverrides(String overrides) {if (overrides != null) {final StringTokenizer parser = new StringTokenizer(overrides, "|", false);final List<String> list = new ArrayList<>(parser.countTokens());while (parser.hasMoreTokens()) {list.add(parser.nextToken().toLowerCase(Locale.ENGLISH));}return list;}return Collections.emptyList();}private class FilteredDynamicContext extends DynamicContext {private DynamicContext delegate;private boolean prefixApplied;private boolean suffixApplied;private StringBuilder sqlBuffer;public FilteredDynamicContext(DynamicContext delegate) {super(configuration, null);this.delegate = delegate;this.prefixApplied = false;this.suffixApplied = false;this.sqlBuffer = new StringBuilder();}public void applyAll() {sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);if (trimmedUppercaseSql.length() > 0) {// 动态加前缀applyPrefix(sqlBuffer, trimmedUppercaseSql);// 动态加后缀applySuffix(sqlBuffer, trimmedUppercaseSql);}// 添加完拼接的前后缀,继续拼接完整的Sqldelegate.appendSql(sqlBuffer.toString());}// 获取当前属性@Overridepublic Map<String, Object> getBindings() {return delegate.getBindings();}// 拼接当前Sql@Overridepublic void appendSql(String sql) {sqlBuffer.append(sql);}@Overridepublic String getSql() {return delegate.getSql();}/*** 拼接前缀处理*/private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {if (!prefixApplied) {prefixApplied = true;if (prefixesToOverride != null) {for (String toRemove : prefixesToOverride) {if (trimmedUppercaseSql.startsWith(toRemove)) {sql.delete(0, toRemove.trim().length());break;}}}}if (prefix != null) {sql.insert(0, " ");sql.insert(0, prefix);}}/*** 拼接后缀处理*/private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {if (!suffixApplied) {suffixApplied = true;if (suffixesToOverride != null) {for (String toRemove : suffixesToOverride) {if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {int start = sql.length() - toRemove.trim().length();int end = sql.length();sql.delete(start, end);break;}}}if (suffix != null) {sql.append(" ");sql.append(suffix);}}}}}
3.3 DynamicSqlSource
在处理完Sql语句需要包装成SqlSource源,之前处理的都是静态源,这次我们需要加动态源类DynamicSqlSource类,实现SqlSource。
1.这个动态源主要是将所有的SqlNode进行处理,拼接为一个处理过的Sql然后直接返回Sql。
2.最后返回SqlSource时判断是否是动态的,是返回DynamicSqlSource,不是返回RawSqlSource
/*** @Author df* @Description: 动态SQL源码* @Date 2023/12/22 11:20*/
// step-15新增
public class DynamicSqlSource implements SqlSource {private Configuration configuration;private SqlNode rootSqlNode;public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {this.configuration = configuration;this.rootSqlNode = rootSqlNode;}@Overridepublic BoundSql getBoundSql(Object parameterObject) {// 生成一个 DynamicContext 动态上下文DynamicContext context = new DynamicContext(configuration, parameterObject);// SqlNode.apply 将 ${} 参数替换掉,不替换 #{} 这种参数rootSqlNode.apply(context);// 调用 SqlSourceBuilderSqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();// SqlSourceBuilder.parse 这里返回的是 StaticSqlSource,解析过程就把那些参数都替换成?了,也就是最基本的JDBC的SQL语句。SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());// SqlSource.getBoundSql,非递归调用,而是调用 StaticSqlSource 实现类BoundSql boundSql = sqlSource.getBoundSql(parameterType);for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());}return boundSql;}// 解析脚本里的动态节点// 更改判断是否动态调用不同的SqlSourcepublic SqlSource parseScriptNode() {// step-15修改List<SqlNode> contents = parseDynamicTags(element);MixedSqlNode rootSqlNode = new MixedSqlNode(contents);SqlSource sqlSource = null;if (isDynamic) {sqlSource = new DynamicSqlSource(configuration, rootSqlNode);} else {sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource;}
}
4.测试准备
dao层:
public interface IActivityDao {Activity queryActivityById(Activity activity);}
Activity_Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.bugstack.mybatis.test.dao.IActivityDao"><resultMap id="activityMap" type="cn.bugstack.mybatis.test.po.Activity"><id column="id" property="id"/><result column="activity_id" property="activityId"/><result column="activity_name" property="activityName"/><result column="activity_desc" property="activityDesc"/><result column="create_time" property="createTime"/><result column="update_time" property="updateTime"/></resultMap><select id="queryActivityById" parameterType="cn.bugstack.mybatis.test.po.Activity" resultMap="activityMap">SELECT activity_id, activity_name, activity_desc, create_time, update_timeFROM activity<trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and"><if test="null != activityId">activity_id = #{activityId}</if></trim></select>
</mapper>
单元测试
public class ApiTest {private Logger logger = LoggerFactory.getLogger(ApiTest.class);private SqlSession sqlSession;@Beforepublic void init() throws IOException {// 1. 从SqlSessionFactory中获取SqlSessionSqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));sqlSession = sqlSessionFactory.openSession();}@Testpublic void test_queryActivityById() throws IOException {// 2. 获取映射器对象IActivityDao dao = sqlSession.getMapper(IActivityDao.class);// 3. 测试验证Activity req = new Activity();req.setActivityId(100001L);Activity res = dao.queryActivityById(req);logger.info("测试结果:{}", JSON.toJSONString(res));}
}
执行单元测试,结果