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/bind
nodeHandlerMap.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 {
@Override
public 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 {
@Override
public 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();
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public String handleToken(String content) {
// 设置 isDynamic 为 true,即调用了这个类就必定是动态 SQL
this.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>
*/
@Override
public boolean apply(DynamicContext context) {
// 如果满足条件,则apply,并返回true
if (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) {
// 非常简单,就是调用ognl
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
// 如果是Boolean
return (Boolean) value;
}
if (value instanceof Number) {
// 如果是Number,判断不为0
return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
}
// 否则判断不为null
return 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);
@Override
public 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;
}
@Override
public 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);
}
// 添加完拼接的前后缀,继续拼接完整的Sql
delegate.appendSql(sqlBuffer.toString());
}
// 获取当前属性
@Override
public Map<String, Object> getBindings() {
return delegate.getBindings();
}
// 拼接当前Sql
@Override
public void appendSql(String sql) {
sqlBuffer.append(sql);
}
@Override
public 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;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 生成一个 DynamicContext 动态上下文
DynamicContext context = new DynamicContext(configuration, parameterObject);
// SqlNode.apply 将 ${} 参数替换掉,不替换 #{} 这种参数
rootSqlNode.apply(context);
// 调用 SqlSourceBuilder
SqlSourceBuilder 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;
}
// 解析脚本里的动态节点
// 更改判断是否动态调用不同的SqlSource
public 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_time
FROM 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;
@Before
public void init() throws IOException {
// 1. 从SqlSessionFactory中获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
sqlSession = sqlSessionFactory.openSession();
}
@Test
public 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));
}
}
执行单元测试,结果