mbatis 中的 mapper 类,在代码层级都是接口类,使用框架的时候,也没有要求我们给出这些接口声明的实现,而是应用开发者编写对应的 xml 文件用于映射。
看到这一步,我想有经验的程序员应该能联想到代理。框架应该是通过动态代理的方式给上述的接口声明方法创建的实现类和方法,并且这个代理创建的方法内容还关联到了 mapper.xml 文件中的 sql 语句和参数注入。
这个具体流程是怎么执行的呢?接下来通过测试类来 debug 一下。
一 测试类 debug
@Test
public void testProxy() throws Exception {
String resource = "db_config/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
Configuration configuration = sqlSessionFactory.getConfiguration();
SqlSession sqlSession = sqlSessionFactory.openSession();
WaterMapper waterMapper = sqlSession.getMapper(WaterMapper.class);
System.out.println(waterMapper.getById(1));
}
代码是基础的 junit 测试流程,通过 sqlSession 获取对应的 mapper。 需要注意的是,这里的 mapper 实际上是 mybatis 帮我们创建的代理类。
这里检索的入口代码应该是
WaterMapper waterMapper = sqlSession.getMapper(WaterMapper.class);
定义在 SqlSession 接口上的 getMapper 方法,有两个类有对应的实现,debug 的时候实际进入的是 SqlSessionManager.getMapper(Class<T> type)
@Override
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
然后进入到了 org.apache.ibatis.session.Configuration#getMapper,传入的参数是类型参数 class 和 sqlSession。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
继续递进到 org.apache.ibatis.binding.MapperRegistry#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 获取用于创建代理类的工厂类,这个工厂类在 mapper 注册的时候初始化
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
// 如果没有找到对应类代理工厂的话,抛出异常
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过代理工厂类创建代理对象,入参为 sqlSession,上面绑定了类信息和 sql 信息
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
mapperRegistry 即 mapper 的注册器类,这个类持有了为每一个 mapper 创建的代理工厂类及其映射关系,key 为对应的 mapper 类型 class 信息。
protected T newInstance(MapperProxy<T> mapperProxy) {
// 3 mapperProxy 中包含了被代理对象,sqlSession 对象和方法缓存,此处直接使用了 jdk 的动态代理
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
// 1 创建一个 mapper 代理对象类
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 2 以创建的 mapper 代理对象类为参数,创建最终供客户端调用的代理对象
return newInstance(mapperProxy);
}
由此可见 mybatis 在此处直接使用了 jdk 的动态代理。不过最后传入的 invocationHandler 类是 mybatis 自己封装的。
public class MapperProxy<T> implements InvocationHandler
这里需要关注一下 mapperProxy 类中复写的 invoke 方法,这里是生成的代理类在方法被调用的时候的核心实现逻辑。
@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);
}
}
org.apache.ibatis.binding.MapperProxy#cachedInvoker
这个方法会返回一个 org.apache.ibatis.binding.MapperProxy.MapperMethodInvoker 对象。这个对象是 MapperProxy 的一个内部接口。实际调用的实现类是
org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker
最后这个代理流程,实际执行到的方法是
org.apache.ibatis.binding.MapperMethod#execute
会根据传入 sql 的类型,执行不同都方法 case。里面实际用到的,还是 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;
}