MyBatis-binding模块与代理模式

MyBatis-binding模块与代理模式

Mybatis通过SqlSession来进行CRUD,其所调用的sql使用id标识存放在xml中,可以通过SqlSession提供的一些方法进行调用,其中一种是传入sql的id与所要使用的参数

1
2
3
4
5
public interface SqlSession extends Closeable {
// ...
<E> List<E> selectList(String statement, Object parameter);
// ...
}

但这样做有一个缺点,经常用ibatis的话,可能深有体会,如果statement填写错了,只能在运行时才能发现,对于开发来说难免会又写错的时候,总会浪费一些时间。

还有一种更加优雅的调用方式,先定义一个Mapper接口,

1
2
3
public interface ProductMapper {
List<Product> queryProduct(Long prodId);
}

建立一个Mapper.xml,接口名对应到Mapper.xml的namespace,接口的方法名对应到xml中的SQL id。

1
2
3
<select id="queryProduct" resultMap="productResultMap">
select xxx from t_product
</select>

调用getMapper获取ProductMapper对象,并调用queryProduct方法即可。

1
2
3
4
5
public interface SqlSession extends Closeable {
// ...
<T> T getMapper(Class<T> type);
// ...
}

这样,可以在mybatis启动时就进行检测,接口是否有对应的sql id存在,规避了运行时找不到对应sql的风险。

归根结底,getMapper接口最终还是调用的SqlSession下的各个select、update方法,只是mybatis将其包装了一下更加方便实用。但是具体是怎么实现的?

主要思路:使用动态代理,增强Mapper接口中的所有方法。调用方法代理时获取在xml中定义的对应的sql语句,同时获取其方法类型,如selectupdatedelete等。最终分别调用SqlSession#selectSqlSession#updateSqlSession#delete等方法。

Mapper的绑定与获取主要由binding模块负责。

image-20210623201716653.png

重要的四个类:

  • MapperRegistry
  • MapperProxyFactory
  • MapperProxy
  • MapperMethod

MapperRegistry

image-20210623202055621

该类注册了所有的Mapper接口以及其对应的被代理的方法。

所包含成员有:

config:All-in-One 的Mybatis全局配置

knowMappers:所有被加载的Mapper,key为Mapper的Class对象,Value为生产Mapper代理的工厂类MapperProxyFactory

追踪SqlSession的getMapper方法发现,

image-20210623220422636

其最终调用的就是MapperRegistry中的getMapper方法。

MapperRegistry.getMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 步骤一
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 步骤二
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

步骤一,从knowMappers中获取该接口对应的代理工厂。如果没有则抛出异常。

步骤二,通过代理工厂生成该接口的代理对象。

MapperProxyFactory

image-20210623222421215

MapperProxyFactory的主要作用就是生成Mapper的Proxy对象。

为什么没有在MapperRegistry直接生成代理对象,而要使用工厂模式?工厂模式的作用是为了屏蔽复杂的对象创建过程。这里生成Proxy,需要调用Proxy对象的构造,其构造参数methodCache也是在Factory中进行初始化的。

成员mapperInterface即是当前需要生产代理类的Mapper的class对象。

成员methodCache用于维护该工厂处理的对应Mapper中的Method与对应的MapperMethodInvoker之间的映射。在使用过程中调用Mapper中的某个方法时,可以拿到该方法对应的具体sql信息。MapperMethodInvoker的具体实现是在MapperProxy中定义的,这里只是新建立相关缓存,并将缓存的引用传递给MapperProxy的构造。

下面看下代理模式是如何具体实践的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 具体生成代理对象的地方
@SuppressWarnings("unchecked")
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);
}

}

通常会调用newInstance(SqlSession sqlSession),传入一个SqlSession生成该工厂对应Mapper的代理。

步骤一,通过传入sqlSession,对应的Mapper的class对象,以及其方法缓存引用,构造一个MapperProxy,这个MapperProxy即是典型的动态代理,实现了InvocationHanler接口,作为代理对象。

步骤二,通过Proxy.newProxyInstance生成动态代理对象。

代理对象需要做的,就是重新实现被代理接口的方法,所以会需要一个入参是被代理接口,即mapperInterface,最后一个入参MapperProxy也一定是InvokationHandler的实现类。具体看下MapperProxy。

MapperProxy

见名知义了,它会成为Mapper接口的具体实现。重点在于invoke方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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);
}
} // ignore catch statement
}

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// ......上面省略一些,下面主要先判断java版本,对default方法做特殊处理,直接执行。
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} // ignore catch statement
} else {
// 如果是普通的方法,则需要做一些具体的操作。
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} // ignore catch statement
}

每个方法会映射到一个执行器MethodInvoker,并将执行器添加到Cache中,方便下一次调用。default方法会执行DefaultMethodInvoker,而普通方法则是调用的PlainMethodInvoker,期间生成对应的方法抽象,即MapperMethod。

MethodInvoker中包含了MapperMethod,这个MapperMethod就是我们常常使用的接口中的具体方法了,最终调用MethodInvoker.invoke方法。invoke的实现一般就是调用MapperMethod的execute方法,execute中会具体调用select,update,delete,insert相关SqlSession操作。

image-20211102223247628

总结

binding模块是一个典型的动态代理的使用案例,通过面向mapper接口,解决启动时检查statement的正确性,调用sql的行为也变得更为优雅,入参也可以直接定义在方法签名中,而不是一味的使用如SqlSession的select方法,传入一个让人捉摸不透的Object参数(参数解析不属于binding模块)。

可见,使用mapper接口的好处是有很多的,动态代理也让一系列复杂的过程变得对开发人员透明,设计思路十分值得学习。譬如我们在写RPC调用时,很多时候也是面向接口api的,通过Proxy,也可以让一系列非业务逻辑代码变得透明,如同调用本地的方法一般进行rpc,开发也可以更聚焦于业务。