简介
这篇文档记录 mybatis 的DAO层接口参数以及方法返回值的处理,方便有需要的时候查找
正文
我们知道在 mybatis 定义一个接口参数可以加 @Param 注解,也可以不加,如果加了注解 sql 里取值就用注解的值,如果不加就默认用参数名称,比如下面的方法定义:
public interface UserMapper {
@Select("select * from user where id = #{id}")
User find(Long id);
@Select("select * from user where id = #{name}")
User find(@Param("name") Long id);
}
以上两种方法定义都可以生效,下面看下mysql里这块处理是怎么做的,因为mysql定义DAO只是定义一个接口,没有实现,mybatis会动态代理生成一个实现,关于这块内容不是本文重点,所以不做介绍,在执行sql方法的时候执行的是动态代理的方法,这里我贴出动态代理方法入口:
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
mapper接口里方法的执行入口会是这里,调用invoke()方法进入cachedInvoker()方法,这个方法里会有一个创建MapperMethod类的地方,该类才是处理mapper接口参数的主要类,下面详细看下:
// 类名意味着该类对应 mapper 接口的一个方法
public class MapperMethod {
// 这里是mapper接口一个方法最终要的两个属性,后面会详细分析
private final SqlCommand command;
private final MethodSignature method;
上面的类中,SqlCommand和MethodSignature 是最重要的两个属性,SqlCommand记录了一个方法的名称和方法类型:
public static class SqlCommand {
// 方法名称
private final String name;
// 方法类型:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
private final SqlCommandType type;
/**
* @param configuration mybatis全局唯一配置
* @param mapper接口 Class,上面定义的UserMapper
* @param method 接口方法,上面定义的find方法
*/
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 解析一个MappedStatement
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 初始化类属性值
// xml文件中sql语句的ID
name = ms.getId();
// xml文件中sql语句的标签类型,INSERT等
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
// 这里是从mybtis启动加载后xml文件中的sql语句中根据id取出这条sql语句
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
// 自定义的mapper文件到这里就执行结束了,下面的for循环是针对像 mbatis plus的 BaseMapper这种有继承关系做的
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
MethodSignature 记录了方法的参数类型和返回类型:
public static class MethodSignature {
private final boolean returnsMany;
private final boolean returnsMap;
private final boolean returnsVoid;
private final boolean returnsCursor;
private final boolean returnsOptional;
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
// 这个参数比较重要,是用来处理方法参数的
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
// 解析方法定义的返回参数
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
// 初始化 参数处理器
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
}
下面看下ParamNameResolver 是如何处理方法参数的
public class ParamNameResolver {
public static final String GENERIC_NAME_PREFIX = "param";
// 方法参数是否使用实际名称,默认true
private final boolean useActualParamName;
// 存放参数下标和下标对应的name
private final SortedMap<Integer, String> names;
// 参数是否有@Param注解
private boolean hasParamAnnotation;
public ParamNameResolver(Configuration config, Method method) {
this.useActualParamName = config.isUseActualParamName();
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++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// 跳过方法参数类型是RowBounds和ResultHandler
continue;
}
// 这个参数代表sql中#{}实际要使用的值
String name = null;
// 这里是参数处理的核心逻辑,获取参数的所有注解
for (Annotation annotation : paramAnnotations[paramIndex]) {
// 如果注解是@Param,name取注解value值,这里只判断@Param注解也就是说mybatis只关心这个注解,其他不关心
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// 到这里说明没找到@Param注解
if (useActualParamName) {
// 默认true,进入这里把name设置为参数本身的名称
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// useActualParamName为false到达这里,使用参数下标作为name,但这里是最不常用的,因为会造成问题,具体不分析了
name = String.valueOf(map.size());
}
}
// 把当前这个位置sql里#{}要使用的参数名称放到map里
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
// 这个方法是根据实际参数获取他要使用的名称
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
// 方法参数为空,直接返回
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
// 只有一个参数并且有@Param注解,获取names第一个值
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
// 这里可能有多个参数或者没有@Param注解
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
}
上面看完 mybatis mapper 接口的方法参数处理核心逻辑集中在ParamNameResolver的构造方法和getNamedParams()方法里。回到 MapperMethod 继续介绍,execute() 方法是 MapperMethod 中最核心的方法之一。execute() 方法会根据要执行的 SQL 语句的具体类型执行 SqlSession 的相应方法完成数据库操作,其核心实现如下:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
对于 INSERT、UPDATE、DELETE 三类 SQL 语句的返回结果,都会通过 rowCountResult() 方法处理。方法的执行结果多数场景中代表了 SQL 语句影响的数据行数,rowCountResult() 方法会将这个 int 值转换成 Mapper 接口方法的返回值,具体规则如下:
- 方法返回void,返回null
- 方法返回Integer,直接返回int值
- 方法返回Long,强转成Long
- 方法返回Boolean,和 0 比较大小后返回
对于SELECT 语句查询到的结果规则如下:
- 方法返回数组或列表,依赖sqlSession.selectList把结果包装成数组或列表
- 方法返回map,依赖sqlSession.selectMap查询结果返回map
- 方法返回cursor,依赖sqlSession.selectCursor查询结果返回cursor
- 方法返回单个对象,依赖sqlSession.selectOne将查询结果返回,如果方法返回Optional,还要在包装一次
以上就是本文的全部内容。