Mybatis插件扩展及与Spring整合原理分析
前言
前面几篇文章分析了Mybatis的核心原理,但模块较多,没有一一分析,更多的需要读者自己下来研究。不过Mybatis的插件扩展机制还是非常重要的,像PageHelper就是一个扩展插件,熟悉其扩展原理,才能更好的针对我们的业务作出更合适的扩展。另外,现在Mybatis都是和Spring/SpringBoot一起使用,那么Mybatis又是如何与它们进行整合的呢?一切答案尽在本文之中。
正文
插件扩展
1.Interceptor核心实现原理
熟悉Mybatis配置的都知道,在xml配置中我们可以配置如下节点:
这个就是插件的配置,那么自然而然的这个节点就会在解析xml的时候进行解析,并将其添加到Configuration中。细心的读者应该还记得下面这段代码,在XMLConfigBuilderl类中:
privatevoidparseConfiguration(XNoderoot){ try{ //issue#117readpropertiesfirst //解析节点 propertiesElement(root.evalNode("properties")); //解析 节点 Propertiessettings=settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); //解析 节点 typeAliasesElement(root.evalNode("typeAliases")); //解析 节点 pluginElement(root.evalNode("plugins")); //解析 节点 objectFactoryElement(root.evalNode("objectFactory")); //解析 节点 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //解析 节点 reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings);//将settings填充到configuration //readitafterobjectFactoryandobjectWrapperFactoryissue#631 //解析 节点 environmentsElement(root.evalNode("environments")); //解析 节点 databaseIdProviderElement(root.evalNode("databaseIdProvider")); //解析 节点 typeHandlerElement(root.evalNode("typeHandlers")); //解析 节点 mapperElement(root.evalNode("mappers")); }catch(Exceptione){ thrownewBuilderException("ErrorparsingSQLMapperConfiguration.Cause:"+e,e); } }
其中pluginElement就是解析插件节点的:
privatevoidpluginElement(XNodeparent)throwsException{ if(parent!=null){ //遍历所有的插件配置 for(XNodechild:parent.getChildren()){ //获取插件的类名 Stringinterceptor=child.getStringAttribute("interceptor"); //获取插件的配置 Propertiesproperties=child.getChildrenAsProperties(); //实例化插件对象 InterceptorinterceptorInstance=(Interceptor)resolveClass(interceptor).newInstance(); //设置插件属性 interceptorInstance.setProperties(properties); //将插件添加到configuration对象,底层使用list保存所有的插件并记录顺序 configuration.addInterceptor(interceptorInstance); } } }
从上面可以看到,就是根据配置实例化为Interceptor对象,并添加到InterceptorChain中,该类的对象被Configuration持有。Interceptor包含三个方法:
//执行拦截逻辑的方法 Objectintercept(Invocationinvocation)throwsThrowable; //target是被拦截的对象,它的作用就是给被拦截的对象生成一个代理对象 Objectplugin(Objecttarget); //读取在plugin中设置的参数 voidsetProperties(Propertiesproperties);
而InterceptorChain只是保存了所有的Interceptor,并提供方法给客户端调用,使得所有的Interceptor生成代理对象:
publicclassInterceptorChain{ privatefinalListinterceptors=newArrayList<>(); publicObjectpluginAll(Objecttarget){ for(Interceptorinterceptor:interceptors){ target=interceptor.plugin(target); } returntarget; } publicvoidaddInterceptor(Interceptorinterceptor){ interceptors.add(interceptor); } publicList getInterceptors(){ returnCollections.unmodifiableList(interceptors); } }
可以看到pluginAll就是循环去调用了Interceptor的plugin方法,而该方法的实现一般是通过Plugin.wrap去生成代理对象:
publicstaticObjectwrap(Objecttarget,Interceptorinterceptor){ //解析Interceptor上@Intercepts注解得到的signature信息 Map,Set >signatureMap=getSignatureMap(interceptor); Class>type=target.getClass();//获取目标对象的类型 Class>[]interfaces=getAllInterfaces(type,signatureMap);//获取目标对象实现的接口 if(interfaces.length>0){ //使用jdk的方式创建动态代理 returnProxy.newProxyInstance( type.getClassLoader(), interfaces, newPlugin(target,interceptor,signatureMap)); } returntarget; }
其中getSignatureMap就是将@Intercepts注解中的value值解析并缓存起来,该注解的值是@Signature类型的数组,而这个注解可以定义class类型、方法、参数,即拦截器的定位。而getAllInterfaces就是获取要被代理的接口,然后通过JDK动态代理创建代理对象,可以看到InvocationHandler就是Plugin类,所以直接看invoke方法,最终就是调用interceptor.intercept方法:
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ try{ //获取当前接口可以被拦截的方法 Setmethods=signatureMap.get(method.getDeclaringClass()); if(methods!=null&&methods.contains(method)){//如果当前方法需要被拦截,则调用interceptor.intercept方法进行拦截处理 returninterceptor.intercept(newInvocation(target,method,args)); } //如果当前方法不需要被拦截,则调用对象自身的方法 returnmethod.invoke(target,args); }catch(Exceptione){ throwExceptionUtil.unwrapThrowable(e); } }
这里的插件实现思路是通用的,即这个interceptor我们可以用来扩展任何对象的任何方法,比如对Map的get进行拦截,可像下面这样实现:
@Intercepts({ @Signature(type=Map.class,method="get",args={Object.class})}) publicstaticclassAlwaysMapPluginimplementsInterceptor{ @Override publicObjectintercept(Invocationinvocation)throwsThrowable{ return"Always"; } @Override publicObjectplugin(Objecttarget){ returnPlugin.wrap(target,this); } @Override publicvoidsetProperties(Propertiesproperties){ } }
然后在使用Map时先用插件对其包装,这样拿到的就是Map的代理对象。
Mapmap=newHashMap(); map=(Map)newAlwaysMapPlugin().plugin(map);
2.Mybatis的拦截增强
因为我们可以对Mybatis扩展任意多个的插件,所以它使用InterceptorChain对象来保存所有的插件,这是责任链模式的实现。那么Mybatis到底会拦截哪些对象和哪些方法呢?回忆上篇文章我们就可以发现Mybatis只会对以下4个对象进行拦截:
Executor:
publicExecutornewExecutor(Transactiontransaction,ExecutorTypeexecutorType){ ......省略 //通过interceptorChain遍历所有的插件为executor增强,添加插件的功能 executor=(Executor)interceptorChain.pluginAll(executor); returnexecutor; }
StatementHandler
publicStatementHandlernewStatementHandler(Executorexecutor,MappedStatementmappedStatement,ObjectparameterObject,RowBoundsrowBounds,ResultHandlerresultHandler,BoundSqlboundSql){ //创建RoutingStatementHandler对象,实际由statmentType来指定真实的StatementHandler来实现 StatementHandlerstatementHandler=newRoutingStatementHandler(executor,mappedStatement,parameterObject,rowBounds,resultHandler,boundSql); statementHandler=(StatementHandler)interceptorChain.pluginAll(statementHandler); returnstatementHandler; }
ParameterHandler
publicParameterHandlernewParameterHandler(MappedStatementmappedStatement,ObjectparameterObject,BoundSqlboundSql){ ParameterHandlerparameterHandler=mappedStatement.getLang().createParameterHandler(mappedStatement,parameterObject,boundSql); parameterHandler=(ParameterHandler)interceptorChain.pluginAll(parameterHandler); returnparameterHandler; }
ResultSetHandler
publicResultSetHandlernewResultSetHandler(Executorexecutor,MappedStatementmappedStatement,RowBoundsrowBounds,ParameterHandlerparameterHandler, ResultHandlerresultHandler,BoundSqlboundSql){ ResultSetHandlerresultSetHandler=newDefaultResultSetHandler(executor,mappedStatement,parameterHandler,resultHandler,boundSql,rowBounds); resultSetHandler=(ResultSetHandler)interceptorChain.pluginAll(resultSetHandler); returnresultSetHandler; }
而具体要拦截哪些对象和哪些方法则是由@Intercepts和@Signature指定的。
以上就是Mybatis扩展插件的实现机制,读者可据此自行分析下PageHelper的实现原理。另外需要注意,我们在进行自定义插件开发时,尤其要谨慎。因为直接关系到操作数据库,如果对插件的实现原理不透彻,很有可能引发难以估量的后果。
Mybatis与Spring整合原理
前面的示例都是单独使用Mybatis,可以看到需要创建SqlSessionFactory和SqlSession对象,然后通过SqlSession去创建Mapper接口的代理对象,所以在与Spring整合时,显而易见的,我们就需要考虑以下几点:
- 什么时候创建以及怎么创建SqlSessionFactory和SqlSession?
- 什么时候创建以及怎么创建代理对象?
- 如何将Mybatis的代理对象注入到IOC容器中?
- Mybatis怎么保证和Spring在同一个事务中并且使用的是同一个连接?
那么如何实现以上几点呢?下文基于mybatis-spring-1.3.3版本分析。
1.SqlSessionFactory的创建
熟悉Spring源码的(如果不熟悉,可以阅读我之前的Spring系列源码)都知道Spring最重要的那些扩展点:
- BeanDefinitionRegistryPostProcessor:Bean实例化前调用
- BeanFactoryPostProcessor:Bean实例化前调用
- InitializingBean:Bean实例化后调用
- FactoryBean:实现该接口代替Spring管理一些特殊的Bean
其它还有很多,以上列举出来的就是Mybatis集成Spring所用到的扩展点。首先我们需要实例化SqlSessionFactory,而实例化该对象在Mybatis里实际上就是去解析一大堆配置并封装到该对象中,所以我们不能简单的使用
进入这个类,我们可以看到它实现了InitializingBean和FactoryBean接口,实现第一个接口的作用就是在该类实例化后立即去执行配置解析的阶段:
publicvoidafterPropertiesSet()throwsException{ notNull(dataSource,"Property'dataSource'isrequired"); notNull(sqlSessionFactoryBuilder,"Property'sqlSessionFactoryBuilder'isrequired"); state((configuration==null&&configLocation==null)||!(configuration!=null&&configLocation!=null), "Property'configuration'and'configLocation'cannotspecifiedwithtogether"); this.sqlSessionFactory=buildSqlSessionFactory(); }
具体的解析就在buildSqlSessionFactory方法中,这个方法比较长,但不复杂,这里就不贴代码了。而实现第二接口的作用就在于Spring获取该类实例时实际上会通过getObject方法返回SqlSessionFactory的实例,通过这两个接口就完成了SqlSessionFactory的实例化。
2.扫描Mapper并创建代理对象
在整合之后我们除了要配置SqlSessionFactoryBean外,还要配置一个类:
这个类的作用就是用来扫描Mapper接口的,并且这个类实现了BeanDefinitionRegistryPostProcessor和InitializingBean,这里实现第二个接口的作用主要是校验有没有配置待扫描包的路径:
publicvoidafterPropertiesSet()throwsException{ notNull(this.basePackage,"Property'basePackage'isrequired"); }
主要看到postProcessBeanDefinitionRegistry方法:
publicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistryregistry){ if(this.processPropertyPlaceHolders){ processPropertyPlaceHolders(); } ClassPathMapperScannerscanner=newClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage,ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
这里创建了一个扫描类,而这个扫描类是继承自Spring的ClassPathBeanDefinitionScanner,也就是会将扫描到的类封装为BeanDefinition注册到IOC容器中去:
publicintscan(String...basePackages){ intbeanCountAtScanStart=this.registry.getBeanDefinitionCount(); doScan(basePackages); //Registerannotationconfigprocessors,ifnecessary. if(this.includeAnnotationConfig){ AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); } return(this.registry.getBeanDefinitionCount()-beanCountAtScanStart); } publicSetdoScan(String...basePackages){ Set beanDefinitions=super.doScan(basePackages); if(beanDefinitions.isEmpty()){ logger.warn("NoMyBatismapperwasfoundin'"+Arrays.toString(basePackages)+"'package.Pleasecheckyourconfiguration."); }else{ processBeanDefinitions(beanDefinitions); } returnbeanDefinitions; } privatevoidprocessBeanDefinitions(Set beanDefinitions){ GenericBeanDefinitiondefinition; for(BeanDefinitionHolderholder:beanDefinitions){ definition=(GenericBeanDefinition)holder.getBeanDefinition(); if(logger.isDebugEnabled()){ logger.debug("CreatingMapperFactoryBeanwithname'"+holder.getBeanName() +"'and'"+definition.getBeanClassName()+"'mapperInterface"); } //themapperinterfaceistheoriginalclassofthebean //but,theactualclassofthebeanisMapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());//issue#59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig",this.addToConfig); booleanexplicitFactoryUsed=false; if(StringUtils.hasText(this.sqlSessionFactoryBeanName)){ definition.getPropertyValues().add("sqlSessionFactory",newRuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed=true; }elseif(this.sqlSessionFactory!=null){ definition.getPropertyValues().add("sqlSessionFactory",this.sqlSessionFactory); explicitFactoryUsed=true; } if(StringUtils.hasText(this.sqlSessionTemplateBeanName)){ if(explicitFactoryUsed){ logger.warn("Cannotuseboth:sqlSessionTemplateandsqlSessionFactorytogether.sqlSessionFactoryisignored."); } definition.getPropertyValues().add("sqlSessionTemplate",newRuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed=true; }elseif(this.sqlSessionTemplate!=null){ if(explicitFactoryUsed){ logger.warn("Cannotuseboth:sqlSessionTemplateandsqlSessionFactorytogether.sqlSessionFactoryisignored."); } definition.getPropertyValues().add("sqlSessionTemplate",this.sqlSessionTemplate); explicitFactoryUsed=true; } if(!explicitFactoryUsed){ if(logger.isDebugEnabled()){ logger.debug("EnablingautowirebytypeforMapperFactoryBeanwithname'"+holder.getBeanName()+"'."); } definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
你可能会好奇,在哪里生成的代理对象?只是将Mapper接口注入到IOC有什么用呢?其实关键代码就在definition.setBeanClass(this.mapperFactoryBean.getClass()),这句代码的作用就是将每一个Mapper接口都转为MapperFactoryBean类型。
为什么要这么转呢?进入这个类你会发现它也是实现了FactoryBean接口的,所以自然而然的又是利用它来创建代理实现类对象:
publicTgetObject()throwsException{ returngetSqlSession().getMapper(this.mapperInterface); }
3.如何整合Spring事务
Mybatis作为一个ORM框架,它是有自己的数据源和事务控制的,而Spring同样也会配置这两个,那么怎么将它们整合到一起呢?而不是在Service类调用Mapper接口时就切换了数据源和连接,那样肯定是不行的。
在使用Mybatis时,我们可以在xml中配置TransactionFactory事务工厂类,不过一般都会使用默认的JdbcTransactionFactory,而当与Spring整合后,默认的事务工厂类改为了SpringManagedTransactionFactory。回到SqlSessionFactoryBean读取配置的方法,在该方法中有下面这样一段代码:
if(this.transactionFactory==null){ this.transactionFactory=newSpringManagedTransactionFactory(); } configuration.setEnvironment(newEnvironment(this.environment,this.transactionFactory,this.dataSource));
上面默认创建了SpringManagedTransactionFactory,同时还将我们xml中ref属性引用的dataSource添加到了Configuration中,这个工厂会创建下面这个事务控制对象:
publicTransactionnewTransaction(DataSourcedataSource,TransactionIsolationLevellevel,booleanautoCommit){ returnnewSpringManagedTransaction(dataSource); }
而这个方法是在DefaultSqlSessionFactory获取SqlSession时会调用:
privateSqlSessionopenSessionFromDataSource(ExecutorTypeexecType,TransactionIsolationLevellevel,booleanautoCommit){ Transactiontx=null; try{ finalEnvironmentenvironment=configuration.getEnvironment(); finalTransactionFactorytransactionFactory=getTransactionFactoryFromEnvironment(environment); tx=transactionFactory.newTransaction(environment.getDataSource(),level,autoCommit); finalExecutorexecutor=configuration.newExecutor(tx,execType); returnnewDefaultSqlSession(configuration,executor,autoCommit); }catch(Exceptione){ closeTransaction(tx);//mayhavefetchedaconnectionsoletscallclose() throwExceptionFactory.wrapException("Erroropeningsession.Cause:"+e,e); }finally{ ErrorContext.instance().reset(); } }
这就保证使用的是同一个数据源对象,但是怎么保证拿到的是同一个连接和事务呢?关键就在于SpringManagedTransaction获取连接是怎么实现的:
publicConnectiongetConnection()throwsSQLException{ if(this.connection==null){ openConnection(); } returnthis.connection; } privatevoidopenConnection()throwsSQLException{ this.connection=DataSourceUtils.getConnection(this.dataSource); this.autoCommit=this.connection.getAutoCommit(); this.isConnectionTransactional=DataSourceUtils.isConnectionTransactional(this.connection,this.dataSource); if(LOGGER.isDebugEnabled()){ LOGGER.debug( "JDBCConnection[" +this.connection +"]will" +(this.isConnectionTransactional?"":"not") +"bemanagedbySpring"); } }
这里委托给了DataSourceUtils获取连接:
publicstaticConnectiongetConnection(DataSourcedataSource)throwsCannotGetJdbcConnectionException{ try{ returndoGetConnection(dataSource); } catch(SQLExceptionex){ thrownewCannotGetJdbcConnectionException("CouldnotgetJDBCConnection",ex); } } publicstaticConnectiondoGetConnection(DataSourcedataSource)throwsSQLException{ Assert.notNull(dataSource,"NoDataSourcespecified"); ConnectionHolderconHolder=(ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource); if(conHolder!=null&&(conHolder.hasConnection()||conHolder.isSynchronizedWithTransaction())){ conHolder.requested(); if(!conHolder.hasConnection()){ logger.debug("FetchingresumedJDBCConnectionfromDataSource"); conHolder.setConnection(dataSource.getConnection()); } returnconHolder.getConnection(); } //Elseweeithergotnoholderoranemptythread-boundholderhere. logger.debug("FetchingJDBCConnectionfromDataSource"); Connectioncon=dataSource.getConnection(); if(TransactionSynchronizationManager.isSynchronizationActive()){ logger.debug("RegisteringtransactionsynchronizationforJDBCConnection"); //UsesameConnectionforfurtherJDBCactionswithinthetransaction. //Thread-boundobjectwillgetremovedbysynchronizationattransactioncompletion. ConnectionHolderholderToUse=conHolder; if(holderToUse==null){ holderToUse=newConnectionHolder(con); } else{ holderToUse.setConnection(con); } holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( newConnectionSynchronization(holderToUse,dataSource)); holderToUse.setSynchronizedWithTransaction(true); if(holderToUse!=conHolder){ TransactionSynchronizationManager.bindResource(dataSource,holderToUse); } } returncon; }
看到ConnectionHolderconHolder=(ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource)这段代码相信熟悉Spring源码的已经知道了,这个我在分析Spring事务源码时也讲过,通过DataSource对象拿到当前线程绑定的ConnectionHolder,这个对象是在Spring开启事务的时候存进去的。至此,关于Spring和Mybatis的整合原理我们就个搞清楚了,至于和SpringBoot的整合,读者可自行分析。最后,我再分享一个小扩展知识。
4.FactoryBean的扩展知识
很多读者可能不知道这个接口有什么作用,其实很简单,当我们有某个类由Spring实例化比较复杂,想要自己控制它的实例化时,就可以实现该接口。而实现该接口的类首先会被实例化并放入一级缓存,而当我们依赖注入我们真正想要的类时(如Mapper接口的代理类),就会从一级缓存中拿到FactoryBean实现类的实例,并判断是否实现了FactoryBean接口,如果是就会调用getObject方法返回我们真正想要的实例。
那如果我们确实想要拿到的就是FactoryBean实现类的实例该怎么办呢?只需要在传入的beanName前面加上“&”符号即可。
总结
本篇分析了Mybatis如何扩展插件以及插件的实现原理,但如非必要,切忌扩展插件,如果一定要,那么一定要非常谨慎。另外还结合Spirng的扩展点分析了Mybatis和Spring的整合原理,解决了困在我心中已久的一些疑惑,相信那也是大多数读者的疑惑,好好领悟这部分内容非常有利于我们自己对Spring进行扩展。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。