Mybatis可以说是目前国内使用最广泛的ORM框架。最原始的使用方式下,我们将sql写在xml配置文件中,通过SqlSession,根据statementId来唯一指定要执行的sql。从Mybatis 3.0之后,我们可以通过一个Mapper映射接口来完成相同的功能。你是否思考过,Mapper映射接口内部是如何完成这样的功能的。本文从源码的角度,深入分析mybatis 映射器接口的工作原理。
在最原始的情况下,我们需要使用SqlSession类,通过namespace.id方式来定位一个sql,如:
String namespace="com.tianshouzhi.mybatis.UserMapper";sqlSession.insert(namespace+".insert",user);sqlSession.selectOne(namespace+".selectById",1);sqlSession.update(namespace+".update",user);sqlSession.delete(namespace+".deleteById",1);
从Mybatis 3.0开始,引入了Mapper映射器接口,我们可以直接通过一个接口来引用需要使用的sql。只要这个接口满足以下条件,即可以引用xml配置文件中的sql:
例如,定义一个UserMapper接口
package com.tianshouzhi.mybatis;public interface UserMapper { public int insert(User user); public User selectById(int id); public int updateById(User user); public int deleteById(int id);}
之后我们就可以直接使用UserMapper类来进行增删改查,如下:
User user=...SqlSession sqlSession = sqlSessionFactory.openSession();try{ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); int insertCount = userMapper.insert(user); user=userMapper.selectById(1); userMapper.updateById(user); userMapper.deleteById(1);} finally { sqlSession.close();}
这里的原理很简单:
当接口方法执行时,首先通过反射拿到当前接口的全路径当做namespace,然后把执行的方法名当成id,拼接成namespace.id,最后在xml映射文件中寻找对应的sql。
在匹配上某个sql之后,底层实际上还是利用SqlSession的相关方法来进行操作,只不过这个过程对于用户来说屏蔽了。
另外,mybatis还会自动的根据Mapper接口方法的返回值类型,选择调用SqlSession的不同方法。例如:
你可能会好奇,Mapper映射接口,是如何完成这些功能的。在接下来的内容中,笔者将从源码角度来分析Mybatis内部是如何使用JDK动态代理机制来完成这些功能,我们带着几个问题开始源码分析之旅:
这个过程在mybatis初始化阶段,解析xml配置文件的时候就确定了。具体逻辑是,当解析一个xml配置文件时,会尝试根据<mapper namespace="....">的namespace属性值,判断classpath下有没有这样一个接口的全路径与namespace属性值完全相同,如果有,则建立二者之间的映射关系。
关解析代码位于XMLMapperBuilder的 parse方法中:
XMLMapperBuilder#parse
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //根据namespace属性值,尝试绑定对应的Mapper接口 bindMapperForNamespace(); } ...}
从bindMapperForNamespace方法名,既可以看出来,其作用正是将Mapper映射器接口绑定到某个xml文件的namespace属性值。具体逻辑如下:
XMLMapperBuilder#bindMapperForNamespace
private void bindMapperForNamespace() { //1 获得mapper元素的namespace属性值 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { //2、通过反射,尝试以namespace属性值为全路径,加载对应Mapper接口的Class对象 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //3、如果没有对应的Mapper接口,将会抛出ClassNotFoundException // 意味着没有对应的Mapper接口,不需要绑定 } if (boundType != null) { if (!configuration.hasMapper(boundType)) { configuration.addLoadedResource("namespace:" + namespace); //4、如果存在这个Mapper,将其添加到Configuration类中 configuration.addMapper(boundType); } } }}
从上述源码的第4步中,调用了Configuration的addMapper方法,来维护需要生成动态代理类的Mapper接口。此外,Configuration还提供了一个getMapper方法,这个方法返回的就是Mapper接口的JDK动态代理类。 相关源码如下所示:
org.apache.ibatis.session.Configuration
public class Configuration {...protected MapperRegistry mapperRegistry = new MapperRegistry(this);...public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type);}public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession);}...}
可以看到,Configuration类实际上将addMapper和getMapper委派给了MapperRegistry来执行:
相关源码如下:
MapperRegistry#addMapper
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();public <T> void addMapper(Class<T> type) { //1 判断传入的type是否是一个接口,如果不是,则忽略。意味着Mapper必须是接口类型。 if (type.isInterface()) { //2、判断Mapper之前是否已经注册过,如果注册过就抛出异常 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { //3、针对Mapper接口的Class对象,生成一个MapperProxyFactoy工厂类,用于之后为这个Mapper接口生成动态代理类 //同时,将Class和MapperProxyFactoy的映射关系放入一个HashMap中,之后根据Class,就可以找到对应的工厂类 knownMappers.put(type, new MapperProxyFactory<T>(type)); //4、解析Mapper映射器接口方法上的注解,如@Select、@Insert等,并进行注册,这里不赘述 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } }}
MapperRegistry还提供了一个getMapper方法,用于根据指定Mapper接口,返回其动态代理类。如下:
MapperRegistry#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //1、首先根据type参数,找到对应的MapperProxyFactory工厂类 final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { //2、通过MapperProxyFactory工厂类,创建这个Mapper接口动态代理 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
注意第2步,在通过MapperProxyFactory创建代理类的时候,把SqlSession当做了参数。这是因为,动态代理类的内部实际上还是需要通过SqlSession来进行增删改查。
每次当我们调用sqlSession的getMapper方法时,都会创建一个新的动态代理类实例。例如有以下代码:
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
这里返回的实际上就是一个动态代理类。其内部实现如下所示:
DefaultSqlSession#getMapper
public class DefaultSqlSession implements SqlSession { private Configuration configuration;... @Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }... }
这里我们看到了SqlSession将将getMapper方法委给了Configuration对象执行。前面已经分析过,在xml解析的时候,就会将Mapper映射接口添加到Configuration内部维护的MapperRegistry中,显然,Configuration的getMapper方法,会继续委派给MapperRegistry来执行。
前面已经看到,MapperRegistry内部是通过已注册MapperProxyFactory的newInstance方法来创建代理,因此这里接着就要分析newInstance方法。
MapperProxyFactory
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; ... public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } ... //1、首先根据sqlSession创建一个MapperProxy对象,MapperProxy实现了JDK动态代理中的InvocationHandler接口 public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } //2、利用JDK提供的Proxy类,创建动态代理。 protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }}
可以看到,MapperProxyFactory的newInstance(sqlSession)方法中,首先会创建一个MapperProxy对象,然后将其当做参数传递给newInstance(mapperProxy)方法,这个方法内部通过JDK提供的Proxy.newProxyInstance方法生成动态代理类。
在JDK动态代理机制中,对方法的拦截是通过回调InvocationHandler接口的invoke方法实现的。在这里,MapperProxy类实现了InvocationHandler接口的invoke方法,因此我们只要从这个方法入手进行分析,既可以得出代理逻辑:
MapperProxy#invoke
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //1 如果当前执行的方法,是在Object类中定义的, //如:equals、hashcode、toString等, //无须对方法进行代理,直接反射执行。 if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } //2、将Mapper接口当前被调用的方法Method包装成一个MapperMethod对象 final MapperMethod mapperMethod = cachedMapperMethod(method); //3 调用MapperMethod的execute方法进行l拦截。 return mapperMethod.execute(sqlSession, args); }
可以看出,对接口方法的核心代理逻辑,显然是位于MapperMethod类execute方法中。之前提到根据Mapper接口全路径+方法名,找到对应的namespace.id,以及根据Mapper方法返回值的不同,执行SqlSession的不同方法,如selectList、selectMap等,都是在这里实现的。
现在我们定位到,最终的拦截代码位于MapperMethod类的execute方法中,当把这个方法的代码分析完成,本文的内容也就分析完成了。
从MapperMethod的构造方法开始看起:
org.apache.ibatis.binding.MapperMethod
public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } ...}
在MapperMethod的构造方法中,给SqlCommand、MethodSignature两个类型的成员变量进行了赋值,这两个类都是MapperMethod的内部类。
这里不对SqlCommand源码继续展开分析,主要关注在构造SqlCommand对象的时候,传入了3个参数:
通过这3个参数,SqlCommand可以为我们提供以下信息:
1 唯一定位当前被调用的Mapper接口的方法,对应的要执行的sql
这个很容易做到,有了mapperInterface,以及method。就可以通过以下方式,来拼接出namespace.id
String statementId = mapperInterface.getName() + "." + methodName;
SqlCommand提供了一个getName方法,返回这个namespace.id。这也是为什么,要求Mapper映射接口,要与xml映射文件namespace属性值相同,方法名与<insert>、<select>等xml元素的id属性值相同的原因。
2 确定要执行的sql的类型
如INSERT、UPDATE、DELETE、SELECT等。因为底层还是通过SqlSession来执行,因此必须知道要执行的sql的类型,选择调用SqlSession的不同方法,如insert、delete、update、selectOne、selectList等。
在第一步确定了要执行的sql的statementId之后,我们可以通过Configuration类来获得这个statementId对应的MappedStatement对象。mybatis在解析xml的过程中,会将<insert>、<select>等xml元素都封装成一个MappedStatement对象,其提供了一个getSqlCommandType()方法,表示这个sql的类型。这个逻辑可以用以下简化后的代码来表示:
String statementId = mapperInterface.getName() + "." + method.getName();MappedStatement ms = configuration.getMappedStatement(statementName);SqlCommandType type= ms.getSqlCommandType();
有了这两个信息之后,我们来看MapperMethod的execute方法是如何执行的?
MapperMethod#execute
public Object execute(SqlSession sqlSession, Object[] args) { Object result; //根据SqlCommand的不同类型,调用sqlSession不同的方法 //1、执行sqlSession.insert if (SqlCommandType.INSERT == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); //2、执行sqlSession.update } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); //3、执行sqlSession.delete } else if (SqlCommandType.DELETE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); //4、对于select,根据Mapper接口方法的返回值类型,选择调用SqlSession的不同方法 } else if (SqlCommandType.SELECT == command.getType()) { if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; //4.1 如果方法的返回值是一个集合,调用selectList方法 } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); //4.2 如果方法的返回值是一个Map,调用selectMap方法 } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); //4.3 如果方法的返回值是Cursor,调用selectCursor方法 } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); //4.4 否则调用sqlSession.selectOne方法 } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } } else if (SqlCommandType.FLUSH == command.getType()) { result = sqlSession.flushStatements(); } else { 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; }
至此,我们已经基本上从源码层面已经深入的分析了Mybatis的Mapper映射接口的内部工作原理,简单总结就是一句话:通过JDK动态代理,根据映射器接口+当前要执行的方法,确定要执行的sql,对sql的类型进行处理,最后还是委派给SqlSession来完成。
需要注意的是:这里的源码分析进行了一定程度上的简化,建议读者还是需要自行阅读源码,加深理解。
另外,本文我们仅仅讨论了单独使用Mybatis时,Mapper映射器接口是如何工作的。在实际开发中,通常Mybatis是与Spring整合的,我们可以在service层通过@Autowired注解,直接注入一个Mapper。这里的核心要点是,如何将Mybatis的Mapper接口变成spring 上下文中的一个bean,只有这样才能支持自动注入。在下一篇文章,笔者将深入分析mybatis-spring的源码,深入剖析MapperScannerConfigurer的内部实现原理,是如何将Mapper接口转换为spring中的bean。