Mybaits源码分析之如何通过只定义Mapper接口实现数据库操作?
在Mybatis的使用中肯定和我一样有很多人会想:为什么Mybatis调用数据库只需要定义了一个接口名为xxxMapper,程序就能正确找到sql,并执行返回我自己定义数据格式,明明我没有实现任何的代码,他到底是如何做到的呢?本章我将介绍他的原理,并手写一套实现代码帮助理解。
回顾
首先来看下之前我们用过的一个代码示例
@Test
public void testGetAll() throws IOException {
String resource = "org/apache/ibatis/demo/user/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println(mapper.getAll());
}
}
而在UserMapper
中就更简单了
public interface UserMapper{
List<User> getAll();
}
在UserMapper.xml中
<mapper namespace="org.apache.ibatis.demo.user.UserMapper">
<select id="getAll" resultMap="userMap">
select id,user_name from user order by id
</select>
</mapper>
请看System.out.println(mapper.getAll());
代码,我们都知道UserMapper.java
并没有实现类,但为什么这里却能实现查询并返回结果呢?之前我们在《Mybatis源码分析(一)》中提到过,这里通过JDK动态代理最终会调用session.selectList()
方法来执行查询,下面我们就来展开讲讲这里的原理!
动态代理过程分析
首先我们顺着session.getMapper
方法进去看到,
org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper()
org.apache.ibatis.session.Configuration#getMapper()
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
这里看到了第一个重要的类MapperRegistry
。
MapperRegiest
初始化注册Mapper
这个类的作用其实就是将所有的Mapper注册进来,并将每一个Mapper转换成代理工厂MapperProxyFactory,使Mapper在调用时能够快速的实例化。
类中的主要使用对象是Map<Class<?>, MapperProxyFactory<?>> knownMappers
,将Mapper类和MapperProxyFactory以键值对的形式存储在HashMap中。
MapperRegiest
的如何注册Mapper?
MapperRegiest
是Configuration
的成员变量,在Configuration
实例话的同时也跟着完成了初始化。
那他是什么时候将Mapper
塞进knownMappers
的呢?我们顺着这个逻辑knownMappers.put()->MapperRegiest.addMapper()->Configuration.addMapper()
,最终谁调用了Configuration.addMapper()
就是完成了Mapper注册。
我们以前讲过的Configuration的初始化,XMLConfigBuilder.parseConfiguration()
实现了解析mybatis-config.xml
文件,并解析文件中的标签,扫描文件时可以有3种方式 resource、url和class,找到对应的xxxMapper.xml文件,解析其中的内容存储为MapperStatement
…
这里为了与我们的示例代码保持一致,扫描的是resource方式。
//解析mybatis-config.xml文件
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration()
//解析mybatis-config.xml文件中的<mapper>标签
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement()
//解析<mapper>标签中resource指向的xxxMapper.xml文件
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse()
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
//解析NameSpace
bindMapperForNamespace();
}
//这里是之前讲过的MapperStatement
parsePendingStatements();
}
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
//通过xxxMapper.xml中配置的namespace得到对应的class类
boundType = Resources.classForName(namespace);
....
//实现Mapper的注册
configuration.addMapper(boundType);
}
}
}
至此我们找到了Mapper注册的代码,其实就是在Configuration
初始化时完成的,通过xxxMapper.xml配置的namespace利用反射找到所有的Mapper类(当然除此以外class、url、扫包等 实现方式不同,但结果是相同的),并存储在HashMap中用于使用。
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
...
knownMappers.put(type, new MapperProxyFactory<>(type));
...
}
}
MapperRegiest
做了什么
回到主线上,
org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper()
org.apache.ibatis.session.Configuration#getMapper()
org.apache.ibatis.binding.MapperRegistry#getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
...
return mapperProxyFactory.newInstance(sqlSession);
...
}
这里的代码其实也很简单,就是从已经注册了所有Mapper类的HashMap中,根据Mapper拿到第二个重要的类:对应的代理工厂 MapperProxyFactory
,调用newInstance()方法实例化。
MapperProxyFactory
代理工厂
MapperProxyFacotry的功能很简单,就是为了生成Mapper的代理对象MapperProxy。
这里的代码很少,我们全部贴出来:
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;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethodInvoker> getMethodCache() {
return methodCache;
}
@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);
}
}
这里的Proxy.newProxyInstance()
就是JDK的动态代理的使用方式了,这里我们主要关注的是实例这个代理类的
MapperProxy
。
MapperProxy
代理的实现
我们应该都知道,JDK动态代理的一定要实现
InvocationHandler
接口,重写invoke()方法。
被代理以后的Mapper类,不管调用哪个方法都会执行invoke()方法,因此我们首先关注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 {
//这里使用MapperMethodInvoker.invoke()方法
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
我们跟踪方法
org.apache.ibatis.binding.MapperProxy#invoke()
org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker#invoke()
org.apache.ibatis.binding.MapperMethod#execute()
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
...
break;
}
case UPDATE: {
...
break;
}
case DELETE: {
...
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//executeForMany()方法里面写了sqlSession.selectList()方法的调用
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
...
}
break;
....
}
return result;
}
我们可以看到这里的几种情况INSERT|UPDATE|DELETE|SELECT就是对应的xxxMapper.xml中我们配置的sql语句对应的标签,如。
接下来的几个方法executeForMany()里面实际上就是调用的sqlSession.selectList()
实现数据查询并返回。sqlSession.selectList()
我们之前《Mybatis源码分析(一)》已经分析过不再重复,本章重点是分析动态代理Mapper。
总结
对整个代理过程作个总结:
- 首先初始化时将所有的被代理的Mapper添加入HashMap,Map中的value是他的代理工厂。
- 执行接口调用时(此时该接口已经是被代理的对象了),找到此代理工厂
- 代理工厂创建代理
- 代理调用invoke()方法
- invoke()方法中找到此方法对应的sql执行。
手写动态代理Mapper
根据我们总结的动态代码Mapper的过程,我们来写一个简单的代理模式,实现Mapper加载。
首先来看下我已经写好的示例:
@Test
public void test(){
SqlSessionT sqlSession = new SqlSessionT();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
System.out.println(userMapper.getOne());
System.out.println(userMapper.getAll());
}
为了区别于Mybatis自带的类,我在类名上加了字母T。
可以看出来调用方法与我们最开始写的Mybatis的是一样的。
程序运行结果:
----------拿到了com.demo.proxy.UserMapper.getOne的SQL------
我是查询结果对象Object!
----------拿到了com.demo.proxy.UserMapper.getAll的SQL------
[我, 是, 查, 询, 结, 果, 列, 表, List]
接下来我把这几个类代码贴出来
MapperProxyT.java
public class MapperProxyT implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("----------拿到"+method.getDeclaringClass().getName()+"."+method.getName()+"的SQL------");
//此处已经通过拿到对应xxxMapper.xml文件中的namespace=+method.getDeclaringClass().getName()
//和所有标签的id=method.getName()
//mybatis通过此拿到对应的sql语句执行查询此处不实现,只模拟结果
if(method.getName().equals("getOne")){
return "我是查询结果对象Object!";
}else if(method.getName().equals("getAll")){
List<String> resultList = Arrays.asList("我","是","查","询","结","果","列","表","List");
return resultList;
}else{
return "无此方法";
}
}
}
MapperProxyFactoryT.java
public class MapperProxyFactoryT<T> {
private Class<T> type;
public MapperProxyFactoryT(Class<T> type) {
this.type = type;
}
public <T> T newInstance(Class<T> type){
//利用jdk动态代理传入的mapper类
return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},new MapperProxyT());
}
}
MapperRegistryT.java
public class MapperRegistryT {
//初始时注册全部的Mapper
private Map<Class<?>, MapperProxyFactoryT<?>> mapperProxyFactoryMap = new HashMap<>();
public <T> void addMapper(Class<T> type){
mapperProxyFactoryMap.put(type,new MapperProxyFactoryT<T>(type));
}
public <T> MapperProxyFactoryT<T> getMapper(Class<T> type){
return (MapperProxyFactoryT<T>)mapperProxyFactoryMap.get(type);
}
}
SqlSessionT.java
public class SqlSessionT {
MapperRegistryT mapperRegiest = new MapperRegistryT();
public SqlSessionT(){
//此处为了模拟Condition初始化时完成对所有Mapper.class的扫描
mapperRegiest.addMapper(UserMapper.class);
}
public <T> T getMapper(Class<T> type){
// if(mapperRegiest.getMapper(type)==null){
// mapperRegiest.addMapper(type);
// }
return mapperRegiest.getMapper(type).newInstance(type);
}
}
以上的类是对整个Mapper代理类的全部实现,下面我们来把用户类与贴出来
UserMapper.java
public interface UserMapper {
String getOne();
List<String> getAll();
}
面试问题
- 如果在接口中定义了多个同名方法,是否能够正确执行呢?
答案:不能正确执行。我们通过上面的代码可以看出来,是通过jdk动态代理最终调用invoke方法来实现,同名方法拿到的sqlid(sqlid=namespace+id),对应到xxxMapper.xml中是只能唯一存在的,不管是重载几次只会执行这一个sql语句。