流程原理分析系列:
MyBatis原理分析之获取SqlSessionFactor

MyBatis原理分析之获取SqlSession

MyBatis原理分析之获取Mapper接口的代理对象

MyBatis原理分析之查询单个对象

本篇博文是原理分析的第三篇。

当使用mapper接口进行CRUD时,其实是其代理对象在发挥作用,SQLsession获取mapper接口的代理对象时序图如下:
MyBatis原理分析之获取Mapper接口的代理对象_原理分析

【1】DefaultSqlSession

如下代码所示,这里其实是调用了configuration实例的方法。该方法是一个泛型方法,参数有​​Class<T> type​​​表示你的接口Class对象,比如​​UserMapper.class---interface com.jane.mapper.UserMapper​

@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}

关于DefaultSqlSession更多详情参考博文:MyBatis中SqlSessionFactory和SqlSession简解

【2】Configuration

configuration有常量成员​​protected final MapperRegistry mapperRegistry = new MapperRegistry(this);​​实例,mapperRegistry 引用了当前configuration。

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}

MapperRegistry属性和构造方法如下:

public class MapperRegistry {

private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

public MapperRegistry(Configuration config) {
this.config = config;
}
//..
}

在MyBatis原理分析之获取SqlSessionFactory一文中我们可以得知创建SqlSessionFactory时对所有的mapper(xml和接口)进行了解析并为每一个mapper接口创建了MapperProxyFactory对象放入knownMappers 中。

MapperRegistry的addMapper方法如下:

public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
//这里向knownMappers放入当前解析的mapper对应的MapperProxyFactory实例
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

【3】MapperRegistry

@SuppressWarnings("unchecked")
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);
}
}

代码解释如下:

  • ① 从knownMappers中获取当前class对象的mapperProxyFactory实例;
    MyBatis原理分析之获取Mapper接口的代理对象_sql_02
  • ② 如果不存在则抛出异常;
  • ③ 如果存在则执行​​mapperProxyFactory.newInstance(sqlSession)​​​来获取当前​​mapper​​的代理对象

【4】MapperProxyFactory

MapperProxyFactory主要属性和构造方法

public class MapperProxyFactory<T> {

private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
//...
}

可以看到其除了接口的class对象外还维护了一个私有ConcurrentHashMap类型常量​​methodCache​​。

创建实例对象MapperProxy

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

代码解释如下:

  • ① 根据​​sqlsession​​​、​​mapperInterface​​​(接口的class对象),以及类型的​​methodCache​​​创建​​MapperProxy​
  • ② 为Mapper创建代理对象

这里需要注意的是​​MapperProxy​​​是一个​​InvocationHandler​​​类型,需要实现​​Object invoke(Object proxy, Method method, Object[] args)​​方法。

MapperProxy的主要属性和构造方法

public class MapperProxy<T> implements InvocationHandler, Serializable {

private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;

public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
//...
}

InvocationHandler是什么呢?InvocationHandler是由代理实例的调用处理程序实现的接口。也就是说一个类/接口的代理实例的调用处理程序必须实现InvocationHandler接口的invoke方法。可以理解为本文中MapperProxy就是Mapper代理实例的调用处理程序。

InvocationHandler的invoke方法如下:

Object invoke(Object proxy, Method method, Object[] args)
proxy:代理实例对象
method:目标方法
args:方法入参

Proxy是什么?Proxy是专门完成代理的操作类,是所有动态代理类的父类,通过此类为一个或多个接口动态地生成实现类。使用 Proxy 生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义。通常都是为指定的目标对象生成动态代理

//直接创建一个动态代理对象
static Object newProxyInstance( ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
loader :定义代理类的类加载器
interfaces:被代理类实现的所有接口
h:代理实例的调用处理程序
该方法将会返回一个代理对象,代理对象有代理调用处理程序--InvocationHandler

根据mapperProxy创建Mapper的代理对象

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

可以看下最终得到的Mapper的代理对象如下(​​h 表示其是一个InvocationHandler也就是调用处理程序​​​):
MyBatis原理分析之获取Mapper接口的代理对象_sql_03
每个代理实例都有一个关联的调用处理程序InvocationHandler。在代理实例上调用方法时,方法调用将被编码并发送到其调用处理程序的​​​invoke​​方法。

关于java的动态代理更多参考博文:
​Java中的代理模式与动(静)态代理Java中动态代理使用与原理详解


【5】MapperMethod

上面我们提到了​​MapperProxyFactory​​​有常量成员​​methodCache​​ ,在类加载过程中就进行了初始化。

private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

根据​​MapperProxyFactory​​​生成​​MapperProxy​​​实例时,将​​ConcurrentHashMap​​类型的methodCache 传了过去。

final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);

​MapperProxy​​​实现了​​InvocationHandler​​​接口的​​invoke​​​方法,那么在使用​​Mapper​​​进行​​CRUD​​​时实际会调用对应的​​MapperProxy​​​的​​invoke​​方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

如上代码可以看到其先获取了MapperMethod 实例,然后调用了MapperMethod 实例的execute方法。

cachedMapperMethod方法

private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}

代码解释如下:

  • ① 尝试从​​ConcurrentHashMap​​​类型的​​methodCache​​​获取当前​​method​​​对应的​​MapperMethod​
  • ② 如果①没有获取到,则新建​​MapperMethod​​实例
  • ③ 将​​{method=mapperMethod}​​​放入​​methodCache​​中
  • ④ 返回MapperMethod 实例

这里先看一下一个Method对象是个什么?
MyBatis原理分析之获取Mapper接口的代理对象_sql_04

​new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())​

构造方法如下:

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}

这里我们可以看到,其新建了SqlCommand和MethodSignature实例。

MapperMethod的UML图如下所示
MyBatis原理分析之获取Mapper接口的代理对象_原理分析_05

再看一下MapperMethod实例对象
MyBatis原理分析之获取Mapper接口的代理对象_处理程序_06

​new SqlCommand(config, mapperInterface, method)​

SqlCommand是MapperMethod静态嵌套类,主要属性是name和SqlCommandType。

public static class SqlCommand {

private final String name;
private final SqlCommandType type;

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = null;
if (configuration.hasStatement(statementName)) {
ms = configuration.getMappedStatement(statementName);
} else if (!mapperInterface.equals(method.getDeclaringClass())) { // issue #35
String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
if (configuration.hasStatement(parentStatementName)) {
ms = configuration.getMappedStatement(parentStatementName);
}
}
if (ms == null) {
if(method.getAnnotation(Flush.class) != null){
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): " + statementName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
//...
}

构造方法如下解释如下

  • ①​​mapperInterface.getName() + "." + method.getName()​​​解析获取到​​statementName​​​ ,如​​com.mybatis.dao.EmployeeMapper.getEmpById​​;
  • ② 判断configuration实例的​​Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")​​成员对象中是否有①中的statementName;
  • ③ 如果存在,则获取statementName对应的MappedStatement;
  • ④ 如果不存在且当前接口Class不是方法的所属Class,则根据方法的所属Class的name与方法名字解析新的statementName
  • ⑤ 如果configuration的​​Map<String, MappedStatement> mappedStatements​​成员中存在新的statementName,则返回对应的MappedStatement

  • ⑥ 如果最终得到的MappedStatement为null,则判断方法上面是否有注解​​@Flush​​;
  • ⑦ 如果有注解​​@Flush​​​,则赋值​​name=null,type = SqlCommandType.FLUSH​​;
  • ⑧ 如果没有注解​​@Flush​​,则抛出异常
  • ⑨ 如果最终得到的MappedStatement不为null,则赋值​​name = ms.getId(); type = ms.getSqlCommandType();​​。
  • ⑩ 如果type为UNKNOWN,则抛出异常

SqlCommandType 是一个枚举类,主要有值​​UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH​

public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

hasStatement(statementName)与getMappedStatement(statementName)

configuration.hasStatement(statementName)) ;
configuration.getMappedStatement(statementName);

上面我们看到这样两句代码,代码表面本身很好理解,我们跟进去看:

hasStatement代码流程片段如下:

public boolean hasStatement(String statementName) {
return hasStatement(statementName, true);
}
public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.containsKey(statementName);
}

getMappedStatement代码流程片段如下:

public MappedStatement getMappedStatement(String id) {
return this.getMappedStatement(id, true);
}

public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
buildAllStatements();
}
return mappedStatements.get(id);
}

​getMappedStatement​​​方法在以下地方有被调用
MyBatis原理分析之获取Mapper接口的代理对象_mybatis_07

可以看到,其都调用了​​buildAllStatements();​​​。那么这个方法是什么呢?如下代码所示,其实就是mybatis提供的​​快速失败​​机制

protected void buildAllStatements() {
parsePendingResultMaps();
if (!incompleteCacheRefs.isEmpty()) {
synchronized (incompleteCacheRefs) {
incompleteCacheRefs.removeIf(x -> x.resolveCacheRef() != null);
}
}
if (!incompleteStatements.isEmpty()) {
synchronized (incompleteStatements) {
incompleteStatements.removeIf(x -> {
x.parseStatementNode();
return true;
});
}
}
if (!incompleteMethods.isEmpty()) {
synchronized (incompleteMethods) {
incompleteMethods.removeIf(x -> {
x.resolve();
return true;
});
}
}
}

在该方法上面有如下注释:

Parses all the unprocessed statement nodes in the cache. It is recommended to call this method once all the mappers are added as it provides fail-fast statement validation.

解析缓存中所有未处理的statement节点。建议在添加所有映射程序后调用此方法,因为它提供​​fail fast​​语句验证。

resolveCacheRef、parseStatementNode、resolve都会抛出异常

那么什么是fail fast 呢?

fail fast即快速失败。这是一种设计思想,即系统如果发现异常立即抛出异常结束任务。与快速失败对应的还有​​fail save​​​也就是安全失败,简单解释即为系统发现异常时,并不抛出异常结束程序,而是捕获异常通常并写入到错误日志中。在java的集合里面可以看到具体实例,参考博文浅谈从fail-fast机制到CopyOnWriteArrayList使用