MyBatis-3.5.11
概述
我们在调用 SqlSession 相应方法执行数据库时,需要指定映射文件中定义的 SQL 节点,如果出现拼写错误,我们只能在运行中才能发现异常。而 MyBatis 通过 binding 模块,将用户自定义的 Mapper 接口与映射配置文件关联起来,如果存在无法关联的 SQL 语句,在 MyBatis 初始化的时候就会报错,从而帮助我们提前发现问题。
其中主要有下面四个类:
MapperRegistry
是 Mapper 接口及其对应的代理对象工厂的注册中心。
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
- config 是 MyBatis 全局唯一的配置对象,其中包含了所有的配置信息
- knownMappers Mapper 接口与 MapperProxyFactory 之间的关系
在 MyBatis 初始化过程中会读取配置文件以及 Mapper 接口中的注解信息,并填充 knownMappers 集合。该集合的 key 是 Mapper 接口对应的 Class 对象,value 是 MapperProxyFactory 对象,可以为 Mapper 接口创建代理对象。
MapperProxyFactory
创建 MapperProxy 动态代理对象
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
- mapperInterface Mapper 接口。
- methodCache 方法与 MapperMethodInvoker 的映射。
其中还有对 java 动态代理的实现:其中 Invocation 参数是 MapperProxy 对象。
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
MapperProxy
MapperProxy 实现了 InvocationHandler 接口,此接口的实现是代理对象的核心逻辑。主要属性如下:
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache;
- SqlSession 记录了关联的 SqlSession 对象
- mapperInterface Mapper 接口对应的 Class 对象
- methodCache 缓存 MapperMethodInvoker 对象,其实最终都是指向 MapperMethod 对象的。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//如果是 Object 定义的方法,直接调用
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);
}
}
MapperProxy.invoke() 核心逻辑,其中 cachedInvoker() 方法里面有一段判断代码,下面也是对MapperMethodInvoker 对象进行缓存了,如果存在直接返回,不存在新建后放到缓存。
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
if (m.isDefault()) { //这里表示在 interface 中使用了 default 方法,显然,代码中对应 SQL 文件的时候,都没有过这种写法。至少我们项目中是这样的,所以都会走 else 逻辑
}else{
mapperMethod.execute
}
MapperMethod
封装了 Mapper 接口中对应方法的信息,以及对应 SQL 语句信息
private final SqlCommand command; //记录 SQL 的名称和类型
private final MethodSignature method;//Mapper 接口中对应方法的相关信息
其中 MethodSignature#convertArgsToSqlCommandParam 会处理入参, 涉及 MyBatis 多入参的问题就是这里处理的:参考我的另一篇文章:juejin.cn/post/722405…
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;
}
上面就是 SQL 执行的不同处理:insert/update/delete 都是返回处理的条数。select 可以返回 executeForMany 多条;executeForMap 方法的返回类型是 Map;executeForCursor 游标查询;selectOne 一条查询。
里面全部都去调用 SqlSession 的方法。
参考:
- 徐郡明 《MyBatis 技术内幕》 的 「2.8 bind 模块」 小节
- 带你手撸 MyBatis