Mybatis Mapper接口工作原理实例解析
KeyWords:Mybatis原理,源码,MybatisMapper接口实现类,代理模式,动态代理,Java动态代理,
Proxy.newProxyInstance,Mapper映射,Mapper实现
MyBatis是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。我们在使用Mybaits进行,通常只需要定义几个Mapper接口,然后在编写一个xml文件,我们在配置文件中写好sql,Mybatis帮我们完成Mapper接口道具体实现的调用。以及将结果映射到modelbean中。
我们在项目中所编写的众多的Mapper类只是一个接口(interface),根据Java的多态性我们知道,可以使用接口接口作为形参,进而在运行时确定具体实现的对象是什么。但是,对于Mapper接口,我们并没有编写其实现类!Mybatis是如何找到其实现类,进而完成具体的CRUD方法调用的呢?原理何在?
Mapper接口是怎么找到实现类的
为了弄清楚Mapper接口是如何找到实现类的,我们先回忆一下Mybatis是怎么使用的,根据实际的例子,进而一点点的去分析。这里的使用指的是Mybatis单独使用,而不是整合spring,因为整合spring的话,还需要涉及Mapperdao装载到spring容器的问题,spring帮忙创建数据源配置等问题。
通常我们使用Mybatis的主要步骤是:
- 构建SqlSessionFactory(通过xml配置文件,或者直接编写Java代码)
- 从SqlSessionFactory中获取SqlSession
- 从SqlSession中获取Mapper
- 调用Mapper的方法,例如:blogMapper.selectBlog(intblogId)
从一段代码看起
上面我们概括了使用Mybatis的4个步骤。这4个步骤看起来很简单,但是用代码写出来就很多。我们不妨先记着这4个步骤,再去看代码,会容易点。
//1. DataSourcedataSource=BlogDataSourceFactory.getBlogDataSource(); TransactionFactorytransactionFactory=newJdbcTransactionFactory(); Environmentenvironment=newEnvironment("development",transactionFactory,dataSource); Configurationconfiguration=newConfiguration(environment); configuration.addMapper(BlogMapper.class);//添加Mapper接口 SqlSessionFactorysqlSessionFactory=newSqlSessionFactoryBuilder().build(configuration); //2. SqlSessionsession=sqlSessionFactory.openSession(); try{ //3. BlogMappermapper=session.getMapper(BlogMapper.class); //4. Blogblog=mapper.selectBlog(1); }finally{ session.close(); }
在这块代码中,第1部分我们使用了Java编码的形式来实现SqlSessionFactory,也可以使用xml。如果使用xml的话,上面的第一部分代码就是这样的:
Stringresource="org/mybatis/example/mybatis-config.xml";//xml内容就不贴了 InputStreaminputStream=Resources.getResourceAsStream(resource); SqlSessionFactorysqlSessionFactory=newSqlSessionFactoryBuilder().build(inputStream);
我们本次的目标是弄清楚“Mapper是如何找到实现类的”,我们注意上面代码3,4的位置:
//3. BlogMappermapper=session.getMapper(BlogMapper.class); //4. Blogblog=mapper.selectBlog(1);
这里mapper可以调用selectBlog(1)这个方法,说明mapper是个对象,因为对象才具有方法行为实现啊。BlogMapper接口是不能实例化的,更没有具体方法实现。我们并没有定义一个类,让它实现BlogMapper接口,而在这里它只是通过调用session.getMapper()所得到的。由此,我们可以推断:肯定是session.getMapper()方法内部产生了BlogMapper的实现类。有什么技术可以根据BlogMapper接口生成了一个实现类呢?想到这里,对于有动态代理使用经验的程序员来说,很容易想到,这背后肯定是基于动态代理技术,具体怎么实现的呢?下面我们来根据源码一探究竟。
Mapper接口的注册
从上面的代码中,我们知道BlogMapper接口的实现类是从session.getMapper中得来的,大概是基于动态代理技术实现。我们既然能够从SqlSession中得到BlogMapper接口的,那么我们肯定需要先在哪里把它放进去了,然后SqlSession才能生成我们想要的代理类啊。上面代码中有这么一行:
configuration.addMapper(BlogMapper.class);
跟着这个addMapper方法的代码实现是这样的:
publicvoidaddMapper(Class type){ mapperRegistry.addMapper(type); }
我们看到这里mapper实际上被添加到mapperRegissry中。继续跟进代码:
publicclassMapperRegistry{ privatefinalMap,MapperProxyFactory>>knownMappers =newHashMap ,MapperProxyFactory>>(); public voidaddMapper(Class type){ if(type.isInterface()){//只添加接口 if(hasMapper(type)){//不允许重复添加 thrownewBindingException("Type"+type+"isalreadyknowntotheMapperRegistry."); } booleanloadCompleted=false; try{ knownMappers.put(type,newMapperProxyFactory (type));//注意这里 MapperAnnotationBuilderparser=newMapperAnnotationBuilder(config,type); parser.parse(); loadCompleted=true; }finally{ if(!loadCompleted){ knownMappers.remove(type); } } } } }
看到这里我们知道上面所执行的configuration.addMapper(BlogMapper.class);其实最终被放到了HashMap中,其名为knownMappers,knowMappers是MapperRegistry类的一个私有属性,它是一个HashMap。其Key为当前Class对象,value为一个MapperProxyFactory实例。
这里我们总结一下:诸如BlogMapper之类的Mapper接口被添加到了MapperRegistry中的一个HashMap中。并以Mapper接口的Class对象作为Key,以一个携带Mapper接口作为属性的MapperProxyFactory实例作为value。MapperProxyFacory从名字来看,好像是一个工厂,用来创建MapperProxy的工厂。我们继续往下看。
Mapper接口的动态代理类的生成
上面我们已经知道,Mapper接口被到注册到了MapperRegistry中——放在其名为knowMappers的HashMap属性中,我们在调用Mapper接口的方法的时候,是这样的:
BlogMappermapper=session.getMapper(BlogMapper.class);
这里,我们跟踪一下session.getMapper()方法的代码实现,这里SqlSession是一个接口,他有两个实现类,一个是DefaultSqlSession,另外一个是SqlSessionManager,这里我们用的是DefaultSqlSession.为什么是DefaultSqlSession呢?因为我们在初始化SqlSessionFactory的时候所调用的SqlSessionFactoryBuilder的build()方法里边配置的就是DefaultSqlSession,所以,我们进入到DefaultSession类中,看看它对session.getMapper(BlogMapper.class)是怎么实现的:
publicclassDefaultSqlSessionimplementsSqlSession{ privateConfigurationconfiguration; @Override publicTgetMapper(Class type){ returnconfiguration. getMapper(type,this);//最后会去调用MapperRegistry.getMapper } }
如代码所示,这里的getMapper调用了configuration.getMapper,这一步操作其实最终是调用了MapperRegistry,而此前我们已经知道,MapperRegistry是存放了一个HashMap的,我们继续跟踪进去看看,那么这里的get,肯定是从这个hashMap中取数据。我们来看看代码:
publicclassMapperRegistry{ privatefinalMap,MapperProxyFactory>>knownMappers=newHashMap ,MapperProxyFactory>>();//Mapper映射 public TgetMapper(Class type,SqlSessionsqlSession){ finalMapperProxyFactory mapperProxyFactory= (MapperProxyFactory )knownMappers.get(type); try{ returnmapperProxyFactory.newInstance(sqlSession);//重点看这里 }catch(Exceptione){ } } }
我们调用的session.getMapper(BlogMapper.class);最终会到达上面这个方法,这个方法,根据BlogMapper的class对象,以它为key在knowMappers中找到了对应的value——MapperProxyFactory(BlogMapper)对象,然后调用这个对象的newInstance()方法。根据这个名字,我们就能猜到这个方法是创建了一个对象,代码是这样的:
publicclassMapperProxyFactory{//映射器代理工厂 privatefinalClass mapperInterface; privateMap methodCache=newConcurrentHashMap (); publicMapperProxyFactory(Class mapperInterface){ this.mapperInterface=mapperInterface; } //删除部分代码,便于阅读 @SuppressWarnings("unchecked") protectedTnewInstance(MapperProxy mapperProxy){ //使用了JDK自带的动态代理生成映射器代理类的对象 return(T)Proxy.newProxyInstance( mapperInterface.getClassLoader(), newClass[]{mapperInterface}, mapperProxy); } publicTnewInstance(SqlSessionsqlSession){ finalMapperProxy mapperProxy=newMapperProxy (sqlSession,mapperInterface,methodCache); returnnewInstance(mapperProxy); } }
看到这里,就清楚了,最终是通过Proxy.newProxyInstance产生了一个BlogMapper的代理对象。Mybatis为了完成Mapper接口的实现,运用了代理模式。具体是使用了JDK动态代理,这个Proxy.newProxyInstance方法生成代理类的三个要素是:
- ClassLoader——指定当前接口的加载器即可
- 当前被代理的接口是什么——这里就是BlogMapper
- 代理类是什么——这里就是MapperProxy
代理模式中,代理类(MapperProxy)中才真正的完成了方法调用的逻辑。我们贴出MapperProxy的代码,如下:
publicclassMapperProxyimplementsInvocationHandler,Serializable{//实现了InvocationHandler @Override publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法 if(Object.class.equals(method.getDeclaringClass())){ try{ returnmethod.invoke(this,args);//注意1 }catch(Throwablet){ throwExceptionUtil.unwrapThrowable(t); } } finalMapperMethodmapperMethod=cachedMapperMethod(method);//使用了缓存 //执行CURD returnmapperMethod.execute(sqlSession,args);//注意2 } }
我们调用的Blogblog=mapper.selectBlog(1);实际上最后是会调用这个MapperProxy的invoke方法。这段代码中,if语句先判断,我们想要调用的方法是否来自Object类,这里的意思就是,如果我们调用toString()方法,那么是不需要做代理增强的,直接还调用原来的method.invoke()就行了。只有调用selectBlog()之类的方法的时候,才执行增强的调用——即mapperMethod.execute(sqlSession,args);这一句代码逻辑。
而mapperMethod.execute(sqlSession,args);这句最终就会执行增删改查了,代码如下:
publicObjectexecute(SqlSessionsqlSession,Object[]args){ Objectresult; if(SqlCommandType.INSERT==command.getType()){//insert处理,调用SqlSession的insert Objectparam=method.convertArgsToSqlCommandParam(args); result=rowCountResult(sqlSession.insert(command.getName(),param)); }elseif(SqlCommandType.UPDATE==command.getType()){//update Objectparam=method.convertArgsToSqlCommandParam(args); result=rowCountResult(sqlSession.update(command.getName(),param)); }elseif(SqlCommandType.DELETE==command.getType()){//delete Objectparam=method.convertArgsToSqlCommandParam(args); result=rowCountResult(sqlSession.delete(command.getName(),param)); }elseif(SqlCommandType.SELECT==command.getType()){ //删除部分代码 }else{ thrownewBindingException("Unknownexecutionmethodfor:"+command.getName()); } //删除部分代码 returnresult; }
再往下一层,就是执行JDBC那一套了,获取链接,执行,得到ResultSet,解析ResultSet映射成JavaBean。
至此,我们已经摸清楚了Blogblog=mapper.selectBlog(1);中,BlogMapper接口调用到得到数据库数据过程中,Mybaitis是如何为接口生成实现类的,以及在哪里出发了最终的CRUD调用。实际上,如果我们在调用Blogblog=mapper.selectBlog(1);之前,把从slqSession中得到的mapper对象打印出来就会看到,输出大概是这样的:
com.sun.proxy.$Proxy17
动态代理没错吧,Java动态代理实在是太美妙了。
总结
上面我们用层层深入的方式摸清楚了Mapper接口是如何找到实现类的。我们分析了Mapper接口是如何注册的,Mapper接口是如何产生动态代理对象的,Maper接口方法最终是如何执行的。总结起来主要就是这几个点:
1.Mapper接口在初始SqlSessionFactory注册的。
2.Mapper接口注册在了名为MapperRegistry类的HashMap中,key=Mapperclassvalue=创建当前Mapper的工厂。
3.Mapper注册之后,可以从SqlSession中get
4.SqlSession.getMapper运用了JDK动态代理,产生了目标Mapper接口的代理对象。
5.动态代理的代理类是MapperProxy,这里边最终完成了增删改查方法的调用。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。