入参#{}的解析
那么如果是#{}该怎么处理呢?
<select id="get" resultType="com.entity.User">
select * from user where id = #{id}
</select>
List<User> get(Integer id);
由上文得知,由于没有${},那么SqlSource就会变成RawSqlSource。在创建RawSqlSource的时候,在构造方法中就会对#{}解析。
RawSqlSource的构造方法。
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
SqlSourceBuilder.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 = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这里用的hander是ParameterMappingTokenHandler,它的作用是将#{XXX}替换成 ?
ParameterMappingTokenHandler.handleToken
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
这时sql就变成了select * from user where id = ?,到这里还只是解析配置文件。在具体执行方法时也要调用getBoundSql方法将参数进行赋值
//RawSqlSource.getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}
StaticSqlSource.getBoundSql,最后调用BoundSql的构造方法,将sql语句,入参等传入
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
之后就要创建数据库连接,进行查询了。回到这个方法SimpleExecutor.prepareStatement。回顾一下,这是创建StatementHandler后做的一些连接数据库的准备操作。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取jdbc数据库连接
Connection connection = getConnection(statementLog);
//一些准备工作,初始化Statement连接
stmt = handler.prepare(connection, transaction.getTimeout());
//使用ParameterHandler处理入参
handler.parameterize(stmt);
return stmt;
}
我们先进入这个方法PreparedStatementHandler.parameterize。
为什么是PreparedStatementHandler之前也说过,因为语句的默认类型是PREPARED, 还有其他的类型如果是CALLABLE,对应CallableStatementHandler,STATEMENT对应SimpleStatementHandler。可以用参数statementType进行设置。
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
DefaultParameterHandler.setParameters.
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//boundSql用来解析我们的sql语句,parameterMappings是我们传入的参数
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
//这里第一个参数就是id
ParameterMapping parameterMapping = parameterMappings.get(i);
//mode属性允许能指定IN,OUT或INOUT参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。
//#{id}默认mode为OUT
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
//这里是boundsql中的额外参数,可以使用拦截器添加,例子放在下文
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
//如果类型处理器中有这个类型,那么直接赋值就行了,例如这里是Integer类型,类型处理器是有的
//那么直接赋值
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//如果不是的会转化为元数据进行处理,metaObject元数据可以理解为用来反射的工具类,可以处理参数的get,set
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//获取类型处理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
//获取数据库类型
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//使用不同的类型处理器向jdbc中的PreparedStatement设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
value如果是空的那么就直接设置为jdbc的空类型,不为空调用具体的类型处理器。
BaseTypeHandler.setParameter。该类是所有typeHandler的父类.如果不为空调用setNonNullParameter,该方法时抽象的,由具体的子类实现。这里使用的是一个相当于路由的的子类UnknownTypeHandler,这个子类可以根据传入的类型,再去找到具体的类型处理器,例如IntegerTypeHander.
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
+ "Cause: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different configuration property. "
+ "Cause: " + e, e);
}
}
}
UnknownTypeHandler.setNonNullParameter
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
handler.setParameter(ps, i, parameter, jdbcType);
}
UnknownTypeHandler.resolveTypeHandler这个方法根据传入的参数类型,找到具体的TypeHandler
private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
TypeHandler<?> handler;
if (parameter == null) {
handler = OBJECT_TYPE_HANDLER;
} else {
handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
// check if handler is null (issue #270)
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
}
return handler;
}
例如如果这个参数是id,Integer类型,那么就会找到IntegerTypeHandler
//IntegerTypeHandler
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
最后还是使用jdbc的PreparedStatement处理参数。
附:自定义的拦截器用来加入参数。
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})
})
public class MyInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler bs = (StatementHandler) invocation.getTarget();
BoundSql boundSql = bs.getBoundSql();
boundSql.setAdditionalParameter("id","1");
return invocation.proceed();
}
}
例4 ${}和#{}都存在的情况
如果是都存在的情况呢?
<select id="findUserByIdAndName" resultType="com.entity.User">
select * from user where id = ${id} AND name = #{name}
</select>
List<User> findUserByIdAndName(@Param("id") Integer id, @Param("name") String name);
结合上文的分析,由于存在${},所以选择的DynamicSqlSource。
DynamicSqlSource.getBoundSql。这个方法上文分析到了rootSqlNode.apply(context);会将${}替换成具体参数。我们接着分析。
public BoundSql getBoundSql(Object parameterObject) {
//parameterObject中有我们方法传入的参数
DynamicContext context = new DynamicContext(configuration, parameterObject);
//解析${}并替换成具体的值
rootSqlNode.apply(context);
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;
}
解析#{},并将其替换成?
//RawSqlSource.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 = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
这样我们的语句就变成了select * from user where id = 1 AND name = ?,然后调用sqlSource.getBoundSql
//StaticSqlSource.getBoundSql
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
最后的处理方式与例3相同,使用jdbc自带的PreparedStatement进行参数处理。
小结
当我们在解析mapper.xml文件时,就会将sql进行第一遍的解析,将其中的全局变量替换成具体的值。
接着进行第二遍的解析,选择不同的SqlSource。这一边的解析不改变语句中的sql内容。
如果语句中包含${},就选择DynamicSqlSource,等待具体执行sql的时候再做处理.如果仅包含#{}类型的,就选择RawSqlSource。RawSqlSource在创建的时候就会有进行一轮的解析,将语句中的#{XXX}替换为 ?(问号)
之后在执行具体的语句才动态的替换,如果之前选择的是DynamicSqlSource,那么进行两次的解析,第一次将${}替换成具体值,第二次解析#{},使用jdbc的PreparedStatement处理。如果选择的是RawSqlSource,那么这条语句就只有#{},直接用PreparedStatement处理。
可以发现,无论什么类型的sql都会被解析了4次。
需要更多教程,微信扫码即可

👆👆👆
别忘了扫码领资料哦【高清Java学习路线图】
和【全套学习视频及配套资料】
本文深入解析Mybatis中#{}参数处理的源码,从SqlSource的选择到PreparedStatement的使用。讲解了DynamicSqlSource如何处理${},RawSqlSource如何预处理#{XXX}为?,并分析了包含两种占位符的特殊情况。总结了Mapper XML文件解析过程和执行时的参数动态替换机制。
832

被折叠的 条评论
为什么被折叠?



