MyBatis 接口参数与 XML SQL 参数实现动态拼接详解
共分为五个步骤:
- 接口参数解析
- 根据参数名获取参数值
- Collection、List、Map 类型参数再次封装
- 接口参数与动态上下文对象绑定
- 参数与 SQL 拼接
- 补充
接口参数解析
代码流程如下:
详情代码段:
ParamNameResolver.class
/**
* @Param config MyBatis 的全局配置对象
*
* @Param method 当前被调用的接口方法,由上一级的动态代理类传入
*/
public ParamNameResolver(Configuration config, Method method) {
// 获取方法的参数类型
final Class<?>[] paramTypes = method.getParameterTypes();
// 获取方法的注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
...
String name = null;
// 如果参数前使用了 @Param 注解,则获取到参数名
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
// 如果参数未使用 @Param 注解
if (name == null) {
// 请留意此处....
// 则使用默认的参数名 useActualParamName 属性默认为 true ,则参数名的格式为:arg0,arg1...
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// 如果 useActualParamName 被配置为 false ,则参数名为索引名,如:"0", "1", ...
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
// 最终 names 的内容如下:
// Map 类型:paramIndex --》 paramName
分析 ParamNameResolver 的构造器方法中可以得出以下结论:
- 当我们使用 @Param 注解时,参数名为注解中设置的值
- 未使用注解时,未设置全局的 useActualParamName 配置时,useActualParamName 默认配置为 true,则参数名会被设置为:arg0,arg1… 格式
- 未使用注解时,设置全局的 useActualParamName = false 后,参数名会被设置为:“0”, “1”, … 格式
根据参数名获取参数值:
下图为获取具体参数值调用流程:
可以看到,分支2 的调用,最终也是调用的 ParamNameResolver 类中的方法进行参数的获取:
ParamNameResolver.class
/**
* @Param args 接口中传入的参数值,数组形式
*
* @return 单个参数,且没有使用 @Param 注解时,直接返回参数值;多个参数时,返回:参数索引--》参数值 类型的 Map
*/
public Object getNamedParams(Object[] args) {
// 获取接口参数的长度
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
// 如果只有一个参数,且没有使用 @Param 注解,则默认通过索引下标获取第一个参数值
return args[names.firstKey()];
} else {
// 如果是多个参数,或者是启用了 @Param 注解
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// entry.getValue() 为参数名,entry.getKey() 为参数索引,args[entry.getKey()] 为参数值
// 如果接口中参数使用了 @Param ,则此处 param 中存入的将是:interface_@Param_value--》args[?]
// 如果接口中参数未使用 @Param ,且是多个参数,则 param 中存入的是:args0--》args[0],args1--》args[1]...
param.put(entry.getValue(), args[entry.getKey()]);
// 并向 param 中添加一组 通用的参数键值: param1 --》args[0],param2 --》args[1]....
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
if (!names.containsValue(genericParamName)) {
// 如果接口中定义的参数没有 paramIndex 格式的,则在此处会将参数在添加一次到 param 中,这次的 key 是 paramIndex 格式的
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
Collection、List、Map 类型参数再次封装
调用流程如下:
// 如果你的 接口参数使用了 @Param 注解,则参数不会被该方法处理
private Object wrapCollection(final Object object) {
// 如果 参数类型 是 Collection ,则给 Map 中添加一个 key 为 collection 类型的键值,同时也是 List 类型,再添加一个 key 为 list 键值
// 可以使用 MyBatis 的动态标签通过 key 值 collection/list/array 来引用此处封装的 集合/Map 类型参数
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
// 如果 参数类型 是 Array 类型,则添加一个 key 为 array 的键值
StrictMap<Object> map = new StrictMap<>();
// 此处与 map.put("list", object); 同理
map.put("array", object);
return map;
}
return object;
}
接口参数与动态上下文对象绑定
接口参数绑定的话,我们使用 MyBatis 的过程中会经常使用与动态标签配合的方式,这里先只介绍动态标签类型的参数绑定过程,流程图如下:
具体我们来看 DynamicSqlSource 类的 getBoundSql(Object parameterObject) 这个方法:
DynamicContext.class
public BoundSql getBoundSql(Object 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);
// 参数转存 到 metaParameters 中
// metaParameters 的值最终是如下格式:
// _parameter -> parameterObject
// _databaseId -> null(根据实际 XML sql 是否设置 databaseId 值来决定)
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
// 对应上图 分支4 中的 --》 XML 中当前 SQL 使用 动态标签
// 当进入该方法时,parameterObject 参数如果为 Collection/Map 类型时,则已经经过 wrapCollection(final Object object) 封装过一次了
public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
// PARAMETER_OBJECT_KEY 值为 “_parameter”
// parameterObject 是在 wrapCollection(final Object object) 中被组装过的
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
参数与 SQL 拼接
流程图如下:
DefaultParameterHandler.class
@Override
public void setParameters(PreparedStatement ps) {
....
// 获取接口参数对象集合
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
// 拿到接口中的参数名
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
// 使用参数名在 metaParameters 中拿到 对应的值
// metaParameters 存在两组值 :key值分别为 _parameter 和 _databaseId
value = boundSql.getAdditionalParameter(propertyName);
}...else {
// 从当前对象的 parameterObject 中拿到对应 propertyName 的值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
try {
// SQL 中对应参数名更换为 value 值
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}....
}
}
}
}
参数拼接的方法为:
ClientPreparedQueryBindings.class
/**
* @Param paramIndex 参数索引
* @Param val 参数值
* @Param type 设置该参数时,Mysql 中设置的数据类型
*
*/
public synchronized final void setValue(int paramIndex, String val, MysqlType type) {
byte[] parameterAsBytes = StringUtils.getBytes(val, this.charEncoding);
setValue(paramIndex, parameterAsBytes, type);
}
到这一步,MyBatis 就完成了接口参数与 XML SQL 的拼接替换,接下来要做的便是将处理过后的 SQL 转为字节流,通过一个 BufferedOutputStream 类型的对象输出到 MySQL 的服务器端了。
补充
在刚开始使用 MyBatis 框架时,肯定都碰到过一下这个异常:
org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
出现这个异常的原因是因为接口方法的参数是一个以上,并且没有通过使用 @Param 注解指定参数名
来看一下这个信息,提示 参数 ‘id’ 没有找到,可用的参数有 : arg1,arg0,param1,param2 这几个,那么,这四个可用参数是在哪一步被放到可用参数列表的呢?
在该篇博客的正文中 **根据参数名获取参数值:**部分关于 ParamNameResolver 类的 getNamedParams 方法的源码注解中,有如下一段:
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// entry.getValue() 为参数名,entry.getKey() 为参数索引,args[entry.getKey()] 为参数值
// 如果接口中参数使用了 @Param ,则此处 param 中存入的将是:interface_@Param_value--》args[?]
// 如果接口中参数未使用 @Param ,且是多个参数,则 param 中存入的是:args0--》args[0],args1--》args[1]...
param.put(entry.getValue(), args[entry.getKey()]);
// 并向 param 中添加一组 通用的参数键值: param1 --》args[0],param2 --》args[1]....
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
if (!names.containsValue(genericParamName)) {
// 如果接口中定义的参数没有 paramIndex 格式的,则在此处会将参数在添加一次到 param 中,这次的 key 是 paramIndex 格式的
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
在该方法中会给多个参数,并且为使用 @Param 注解的接口参数进行两遍赋值;
- 第一遍赋值 :key 的格式是从 names 中拿到的,names 是在 接口参数解析一节中的 ParamNameResolver 类的 ParamNameResolver 方法中被赋值,赋值关键过程如下:
// 如果参数未使用 @Param 注解
if (name == null) {
// 请留意此处....
// 则使用默认的参数名 useActualParamName 属性默认为 true ,则参数名的格式为:arg0,arg1...
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// 如果 useActualParamName 被配置为 false ,则参数名为索引名,如:"0", "1", ...
name = String.valueOf(map.size());
}
}
...
names = Collections.unmodifiableSortedMap(map);
- 第二遍赋值:
if (!names.containsValue(genericParamName)) {
// 如果接口中定义的参数没有 paramIndex 格式的,则在此处会将参数在添加一次到 param 中,这次的 key 是 paramIndex 格式的
param.put(genericParamName, args[entry.getKey()]);
}
两边赋值过后,可用参数名的列表为:[arg1, arg0, param1, param2] ,而 ‘id’ 不在可用参数列表中,所以就会产生参数没有被找到的异常;要解决该异常,推荐通过给多个接口参数分别增加 @Param 注解来指定参数名,同时,日常开发中,当接口参数为多个是,也推荐使用 @Param 注解来进行参数绑定;虽然 MyBatis 会自动设置一些参数名,不过往往不利于代码的理解。