mybatis中的#{}参数我们最常用的特性,在mybatis中#{}参数最终会作为编译参数来处理,也就是会被替换为‘?’,然后使用PreparedStatement的setXXX方法设置参数值,所以使用#{}参数没有sql注入的风险。
我们先简单回顾一下JDBC预编译语句的使用:
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, password);
PreparedStatement ps = connection.prepareStatement("select name from user where id = ?");
ps.setInt(1, 1);
ResultSet resultSet = ps.executeQuery();
while (resultSet.next()) {
System.out.println("name = " + resultSet.getString("name"));
在使用jdbc的预编译时,我们先将语句中的参数指定为‘?’,在执行前通过setXXX方法为参数指定值。mybatis也是同样的做法,先将#{}替换为?号,然后#{}指定的类型信息来选择对应的set方法进行参数设定。
再简单回顾一下mybatis中#{}的使用:
下面例子为从一个简单的user表中根据user的id查询user的所有信息。
Mapper配置:
<mapper namespace="cn.**.UserMapper">
<select id="queryUserById" resultType="Map">
select * from user
<where>
<if test="id != null">
id = #{value, javaType=int, jdbcType=NUMERIC}
</where>
limit 1
</select>
</mapper
执行查询:
List<?> datas = sqlSession.selectList("queryUserById", 1);
在我们执行queryUserById语句时,mybatis会先将#{value}替换为?,再通过setInt将1给到id参数。
一、解析#{}参数
前面几篇介绍了mybatis如何生成可执行sql和如何进行${}字符串替换,在完成了这两步后就得到了一个只剩余#{}参数需要解析的sql。#{}参数由SqlSourceBuilder进行解析,SqlSourceBuilder在解析时会将#{}替换为‘?’号并将#{}中的内容解析为ParameterMapping的封装,ParameterMapping包含了参数的各个属性。此外SqlSourceBuilder会为语句生成StaticSqlSource,StaticSqlSource代表不包含任何内容的动态内sql,也就是可执行sql。
public class SqlSourceBuilder extends BaseBuilder {
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());
1.1 参数说明
SqlSourceBuilder在解析时需要三个参数:
originalSql
要解析的sql,该sql已经解析过${}字符串引用
parameterType
语句的参数的类型,分为两种情况:
(1) 若语句是一个静态语句(不包含${}和动态标签的语句),该参数值为parameterType配置项的指定的类型:
<select id="queryUserById" resultType="Map" parameterType="cn.***.Test">
</select>
对于这个语句SqlSourceBuilder中parameterType参数的值就为‘cn.***.Test’,若未指定parameterType配置则为Object.class。
(2) 语句非静态语句,那么parameterType就为使用sqlSession执行sql时指定的参数的类型:
sqlSession.selectList("queryUserById", 1);
对于这个查询,SqlSourceBuilder中parameterType类型就为Integer.class。
additionalParameters
额外参数,这里也分为分为两种情况:
(1) 若语句是一个静态语句(不包含${}和动态标签的语句),additionalParameters的值为一个空的HashMap。
(2) 语句非静态语句,那么该参数的值就为使用sqlSession执行sql时指定的参数的ognl上下文,在这个上下文map中包含了两个固定的属性_parameter和_databaseId以及使用bind添加的属性。
public class Test {
private int id = 1;
private String name = "zhangsan";
<select id="queryUserById" resultType="Map" parameterType="cn.***.test.mybatis.Test">
<bind name="userName" value="name" />
select * from user where id = #{id} and name =#{userName} limit 1
</select>
1.2 解析过程
解析是会逐个解析#{}中的内容,为每个参数生成一个ParameterMapping对象,ParameterMapping包含了#{}支持的各个配置项目:
public class ParameterMapping {
private String property;
private ParameterMode mode;
private Class<?> javaType = Object.class;
private JdbcType jdbcType;
private Integer numericScale;
private TypeHandler<?> typeHandler;
private String resultMapId;
private String jdbcTypeName;
解析是除了typeHandler和javaType外其它属性都直接从配置中获取到然后设定,若未指定则为null。
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private ParameterMapping buildParameterMapping(String content) {
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
ParameterMapping中的typeHandler和javaType是必须的并且很重要,因为在后面设置参数值时,具体使用PreparedStatement哪一个set方法是由typeHandler决定的,而typeHandler是通过javaType获取到的。
解析javaType按以下方式:
additionalParameters是否有属性,若有则获取additionalParameters中属性的set方法的类型。
1,不存在,则查找parameterType是否有该类型的TypeHandler,mybatis默认自带了一些基础类型的TypeHandler
2,不存在,则看#{}中配置的javaType的类型是否为CURSOR,若为CURSOR则javaType为java.sql.ResultSet.class
3,不存在,则看属性是否为指定或parameterType是否为Map类型,若是则类型为Object.class
4,不存在,则parameterType指定的类型是否有该属性的get方法,若有则取get方法的返回类型
若以上都不存在,则取属性的java类型取Object.class
若指定了javaType属性则直接取指定的值。
若指定了typeHandler则直接获取指定的类型,若未指定则通过上面步骤获取到javaType查找该类型的typeHandler。
private ParameterMapping buildParameterMapping(String content) {
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
if (metaParameters.hasGetter(property)) {
propertyType = metaParameters.getSetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)){
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
return builder.build();
至此sql中所有的#{}参数都被解析为了ParameterMapping,并被替换为了‘?’,接下来再将替换了#{}的sql和所有的ParameterMapping封装到StaticSqlSource中,由StaticSqlSource提供后续的支持。
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
二、什么时候进行#{}解析
上面介绍了解析的过程,那什么触发#{}的解析那?
#{}的解析时机分为两种情况,静态语句和动态态语句。在SqlSessionFactoryBuilder().build()初始化mybatis时会解析所有配置的语句,对于动态语句会被解析为DynamicSqlSource而静态语句解析为RawSqlSource,这两者都是SqlSource代表了语句的sql,最终都是通过SqlSourceBuilder解析#{}参数并生成StaticSqlSource。
对于RawSqlSource由于不包含动态内容,所以在解析阶段就可以得到最终的sql,故可以在初始时解析#{}参数并生成StaticSqlSource,所以比DynamicSqlSource执行会快一些。
而对于DynamicSqlSource由于包含动态内容所以要在执行是才能知道要执行的sql,也就是到执行时才能知道需哪些#{}会被使用到,所以DynamicSqlSource在语句执行时#{}参数才会被解析。
三、设定参数值
mybatis中语句有STATEMENT,PREPARED和CALLABLE三种类型,分别对应JDBC的Statement,PreparedStatement或 CallableStatement,这三种类型在mybatis中有三种类型的有对应的StatementHandler进行特殊处理,分别为SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler, StatementHandler中定义了处理与编译参数菜单接口parameterize,在执行语句之前会调用该接口进行设置参数值。
public interface StatementHandler {
void parameterize(Statement statement)
throws SQLException;
SimpleStatementHandler对的实现为空方法,所以若语句中包含#{}参数就需要将语句类型设定为PREPARED或CALLABLE,在StatementHandler中使用ParameterHandler来设置参数,ParameterHandler接口中setParameters方法设置。对于xml配置的语句其为DefaultParameterHandler。
DefaultParameterHandler在设置参数时,先获取到参数的值,然后使用ParameterMapping中之前解析到对应的typeHandler来设置值,最终如何设置取决与typeHandler的setParameter设置,例如String的StringTypeHandler使用setString方法:
public class StringTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
DefaultParameterHandler会按顺序遍历所有的ParameterMapping,逐个进行设置。在设置时先获取属性的值,获取参数值时先从额外参数中获取,也就是bind值得参数,若额外参数中没有则看传递的参数是否有对应TypeHandler,若用则直接取传递的参数值,若也没有则任务当前属性为传递的参数中的一个属性调用get方法从传递的参数中获取。
public class DefaultParameterHandler implements ParameterHandler {
public void setParameters(PreparedStatement ps) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() == ParameterMode.OUT) {
continue;
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
至此语句中的#{}参数就解析并设置完成,也得到了一个可执行的语句,后面就执行该语句并解析结果了。
作者:蛋不炒饭
链接:https://juejin.cn/post/6915016292433051655