这篇文章我们将继续探讨MyBatis的BoundSql对象。
系列文章
MyBatis原理系列(一)-手把手带你阅读MyBatis源码
MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程
MyBatis原理系列(三)-手把手带你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系
MyBatis原理系列(四)-手把手带你了解MyBatis的Executor执行器
MyBatis原理系列(五)-手把手带你了解Statement、StatementHandler、MappedStatement间的关系
MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程
MyBatis原理系列(七)-手把手带你了解如何自定义插件
MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存
MyBatis原理系列(九)-手把手带你了解MyBatis事务管理机制
1. BoundSql 初识
在前几篇文章中我们从启动流程到SqlSession
再到Executor
执行器,以及在执行数据库操作的时候都会通过StatementHandler
进行Statement
处理,并且会用到BoundSql
,这个BoundSql
到底是何作用,我们将在此篇文章中讲解下去。
BoundSql
中就是对解析后的sql描述,包括对动态标签的解析,并且将 #{} 解析为占位符 ? ,还包含参数的描述信息。这个类没有什么复杂操作,可以看作是解析后的sql描述对象。
/**
* An actual SQL String got from an {@link SqlSource} after having processed any dynamic content.
* The SQL may have SQL placeholders "?" and an list (ordered) of an parameter mappings
* with the additional information for each parameter (at least the property name of the input object to read
* the value from).
* <p>
* Can also have additional parameters that are created by the dynamic language (for loops, bind...).
*
* 经过处理一些动态sql的部分获取到的真实sql,这个sql可能还有占位符?和一个参数映射的有序集合,并且还有每个参数的额外信息
* @author Clinton Begin
*/
public class BoundSql {
// 最终解析的sql,Mybatis将#{}和${}解析后的sql,其中#{}会被解析为?
private final String sql;
// 参数映射
private final List<ParameterMapping> parameterMappings;
// 参数对象
private final Object parameterObject;
// 额外的参数
private final Map<String, Object> additionalParameters;
// 元数据参数
private final MetaObject metaParameters;
public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.parameterObject = parameterObject;
this.additionalParameters = new HashMap<>();
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public String getSql() {
return sql;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public Object getParameterObject() {
return parameterObject;
}
public boolean hasAdditionalParameter(String name) {
String paramName = new PropertyTokenizer(name).getName();
return additionalParameters.containsKey(paramName);
}
public void setAdditionalParameter(String name, Object value) {
metaParameters.setValue(name, value);
}
public Object getAdditionalParameter(String name) {
return metaParameters.getValue(name);
}
}
2. BoundSql 创建流程
BaseExecutor
的query方法为例,首先会从MappedStatement
对象中获取BoundSql
对象,而MappedStatement
对象我们在上篇文章中讲过其实是sql标签的描述,BoundSql
就是解析后的sql语句。
// BaseExecutor 的query()方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 从MappedStatement对象中获取BoundSql对象
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
获取BoundSql
对象其实是从MappedStatement
的成员变量sqlSource
中获取的。
而SqlSource
作为一个接口,它只有一个作用就是获取BoundSql
对象。
// MappedStatement 的 getBoundSql() 方法
public BoundSql getBoundSql(Object parameterObject) {
// 获取BoundSql对象,BoundSql对象是对动态sql的解析
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 获取参数映射
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings()) {
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null) {
hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
SqlSource
接口只有一个方法,就是获取BoundSql
对象,SqlSource
接口的设计满足单一职责原则。SqlSource
有五个实现类:ProviderSqlSource
,DynamicSqlSource
,RawSqlSource
,StaticSqlSource
,StaticSqlSource
其中比较常用的就是DynamicSqlSource
,RawSqlSource
和StaticSqlSource
。如果sql中只包含#{}参数,不包含${}或者其它动态标签,那么创建SqlSource
对象时则会创建RawSqlSource
,否则创建DynamicSqlSource
对象。
/**
* Represents the content of a mapped statement read from an XML file or an annotation.
* It creates the SQL that will be passed to the database out of the input parameter received from the user.
* 表示一个从xml文件和注解中读取的一个映射语句
* 可以创建一个包含用户输入的参数的,并可以发送到数据库sql
* @author Clinton Begin
*/
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
DynamicSqlSource 会优先解析{}标签,然后解析#{}标签。其中{}会被解析为参数内容,不会加上双引号,而#{}会被解析为?,并且参数会加上双引号。
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 优先解析${}标签
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
// 创建sqlSource对象,并且解析#{}为?
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
rootSqlNode有很多实现,我们看下TextSqlNode的实现。
TextSqlNode
中对${}标签进行了解析,没有加上双引号。
// TextSqlNode 类
@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().get("_parameter");
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); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
然后 sqlSourceParser.parse
解析的是#{}标签,实现如下会将#{}解析为?,具体实现就不看了,有点复杂。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
综上,通过SqlSource接口获取BoundSql对象的时候,会解析动态sql。
3. 例子
3. 1 例1
我们以前面几篇文章的例子,在mapper/TTestUserMapper.xml中配置了以下sql标签
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_test_user
where id = #{id,jdbcType=BIGINT}
</select>
通过打断点,我们看到#{}标签被解析为?占位符,参数的设置就交DBMS去做了。
ParameterMapping 的结构如下,是对参数的描述信息。
ParameterMapping{property='id', mode=IN, javaType=class java.lang.Long, jdbcType=BIGINT, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
3. 2 例2
在mapper/TTestUserMapper.xml
中配置了以下sql标签,其中字段名为动态传入的,使用${}标签进行包裹。
<select id="selectDynamicField" resultMap="BaseResultMap">
select
${fieldName}
from t_test_user
where id = #{id,jdbcType=BIGINT}
</select>
在 com/example/demo/dao/TTestUserMapper.java
下新增以下接口
TTestUser selectDynamicField(@Param("fieldName") String fieldName, @Param("id") Long id);
修改测试代码
public static void main(String[] args) {
try {
// 1. 读取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 创建SqlSessionFactory工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
// 4. 获取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 执行接口方法
TTestUser userInfo = userMapper.selectDynamicField("real_name", 12L);
System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
// 6. 提交事物
sqlSession.commit();
// 7. 关闭资源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
调用此接口查询后,可发现fieldName直接被替换为参数,而id还是被解析为?。
可见${}安全隐患比较大,而#{}可以有效防御sql注入。
4. 总结
通过这篇文章,我们了解SqlSource的几个默认实现,以及{}和#{}的区别,能用#{}时,尽量不要使用{}。我们还了解了BoundSql的结构和创建过程。对此我们对MyBatis的sql执行有了更深层次的了解。