内容简介:@目录前面几篇文章分析了Mybatis的核心原理,但模块较多,没有一一分析,更多的需要读者自己下来研究。不过Mybatis的插件扩展机制还是非常重要的,像PageHelper就是一个扩展插件,熟悉其扩展原理,才能更好的针对我们的业务作出更合适的扩展。另外,现在Mybatis都是和Spring/SpringBoot一起使用,那么Mybatis又是如何与它们进行整合的呢?一切答案尽在本文之中。
@
目录
前言
前面几篇文章分析了Mybatis的核心原理,但模块较多,没有一一分析,更多的需要读者自己下来研究。不过Mybatis的插件扩展机制还是非常重要的,像PageHelper就是一个扩展插件,熟悉其扩展原理,才能更好的针对我们的业务作出更合适的扩展。另外,现在Mybatis都是和Spring/SpringBoot一起使用,那么Mybatis又是如何与它们进行整合的呢?一切答案尽在本文之中。
正文
插件扩展
1. Interceptor核心实现原理
熟悉Mybatis配置的都知道,在xml配置中我们可以配置如下节点:
<plugins> <plugin interceptor="org.apache.ibatis.builder.ExamplePlugin"> <property name="pluginProperty" value="100"/> </plugin> </plugins>
这个就是插件的配置,那么自然而然的这个节点就会在解析xml的时候进行解析,并将其添加到 Configuration 中。细心的读者应该还记得下面这段代码,在 XMLConfigBuilder l类中:
private void parseConfiguration(XNode root) { try { //issue #117 read properties first //解析<properties>节点 propertiesElement(root.evalNode("properties")); //解析<settings>节点 Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); //解析<typeAliases>节点 typeAliasesElement(root.evalNode("typeAliases")); //解析<plugins>节点 pluginElement(root.evalNode("plugins")); //解析<objectFactory>节点 objectFactoryElement(root.evalNode("objectFactory")); //解析<objectWrapperFactory>节点 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //解析<reflectorFactory>节点 reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings);//将settings填充到configuration // read it after objectFactory and objectWrapperFactory issue #631 //解析<environments>节点 environmentsElement(root.evalNode("environments")); //解析<databaseIdProvider>节点 databaseIdProviderElement(root.evalNode("databaseIdProvider")); //解析<typeHandlers>节点 typeHandlerElement(root.evalNode("typeHandlers")); //解析<mappers>节点 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
其中 pluginElement 就是解析插件节点的:
private void pluginElement(XNode parent) throws Exception { if (parent != null) { //遍历所有的插件配置 for (XNode child : parent.getChildren()) { //获取插件的类名 String interceptor = child.getStringAttribute("interceptor"); //获取插件的配置 Properties properties = child.getChildrenAsProperties(); //实例化插件对象 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); //设置插件属性 interceptorInstance.setProperties(properties); //将插件添加到configuration对象,底层使用list保存所有的插件并记录顺序 configuration.addInterceptor(interceptorInstance); } } }
从上面可以看到,就是根据配置实例化为 Interceptor 对象,并添加到 InterceptorChain 中,该类的对象被 Configuration 持有。 Interceptor 包含三个方法:
//执行拦截逻辑的方法 Object intercept(Invocation invocation) throws Throwable; //target是被拦截的对象,它的作用就是给被拦截的对象生成一个代理对象 Object plugin(Object target); //读取在plugin中设置的参数 void setProperties(Properties properties);
而 InterceptorChain 只是保存了所有的 Interceptor ,并提供方法给客户端调用,使得所有的 Interceptor 生成 代理对象 :
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
可以看到 pluginAll 就是循环去调用了 Interceptor 的 plugin 方法,而该方法的实现一般是通过 Plugin.wrap 去生成代理对象:
public static Object wrap(Object target, Interceptor interceptor) { //解析Interceptor上@Intercepts注解得到的signature信息 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass();//获取目标对象的类型 Class<?>[] interfaces = getAllInterfaces(type, signatureMap);//获取目标对象实现的接口 if (interfaces.length > 0) { //使用jdk的方式创建动态代理 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; }
其中 getSignatureMap 就是将@Intercepts注解中的value值解析并缓存起来,该注解的值是@Signature类型的数组,而这个注解可以定义class 类型 、 方法 、 参数 ,即 拦截器的定位 。而 getAllInterfaces 就是获取要被代理的接口,然后通过JDK动态代理创建代理对象,可以看到 InvocationHandler 就是 Plugin 类,所以直接看 invoke 方法,最终就是调用 interceptor.intercept 方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //获取当前接口可以被拦截的方法 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) {//如果当前方法需要被拦截,则调用interceptor.intercept方法进行拦截处理 return interceptor.intercept(new Invocation(target, method, args)); } //如果当前方法不需要被拦截,则调用对象自身的方法 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
这里的插件实现思路是通用的,即这个 interceptor 我们可以用来扩展任何对象的任何方法,比如对 Map 的 get 进行拦截,可像下面这样实现:
@Intercepts({ @Signature(type = Map.class, method = "get", args = {Object.class})}) public static class AlwaysMapPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { return "Always"; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
然后在使用 Map 时先用插件对其包装,这样拿到的就是Map的代理对象。
Map map = new HashMap(); map = (Map) new AlwaysMapPlugin().plugin(map);
2. Mybatis的拦截增强
因为我们可以对 Mybatis 扩展任意多个的插件,所以它使用 InterceptorChain 对象来保存所有的插件,这是 责任链模式 的实现。那么 Mybatis 到底会拦截哪些对象和哪些方法呢?回忆上篇文章我们就可以发现 Mybatis 只会对以下4个对象进行拦截:
- Executor :
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { ......省略 //通过interceptorChain遍历所有的插件为executor增强,添加插件的功能 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
- StatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { //创建RoutingStatementHandler对象,实际由statmentType来指定真实的StatementHandler来实现 StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
- ParameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; }
- ResultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; }
而具体要拦截哪些对象和哪些方法则是由@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里实际上就是去解析一大堆配置并封装到该对象中,所以我们不能简单的使用<bean>标签来配置,为此Mybatis实现了一个类 SqlSessionFactoryBean (这个类我们在以前使用整合包时都会配置),之前XML中的配置都以属性的方式放入到了该类中:
<bean id="sqlSessionFactory"> <property name="dataSource" ref="dataSource" /> <property name="typeAliasesPackage" value="com.enjoylearning.mybatis.entity" /> <property name="mapperLocations" value="classpath:sqlmapper/*.xml" /> </bean>
进入这个类,我们可以看到它实现了 InitializingBean 和 FactoryBean 接口,实现第一个接口的作用就是在该类实例化后立即去执行 配置解析 的阶段:
public void afterPropertiesSet() throws Exception { notNull(dataSource, "Property 'dataSource' is required"); notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), "Property 'configuration' and 'configLocation' can not specified with together"); this.sqlSessionFactory = buildSqlSessionFactory(); }
具体的解析就在 buildSqlSessionFactory 方法中,这个方法比较长,但不复杂,这里就不贴代码了。而实现第二接口的作用就在于Spring获取该类实例时实际上会通过 getObject 方法返回 SqlSessionFactory 的实例,通过这两个接口就完成了 SqlSessionFactory 的实例化。
2. 扫描Mapper并创建代理对象
在整合之后我们除了要配置 SqlSessionFactoryBean 外,还要配置一个类:
<bean> <property name="basePackage" value="com.enjoylearning.mybatis.mapper" /> </bean>
这个类的作用就是用来扫描Mapper接口的,并且这个类实现了 BeanDefinitionRegistryPostProcessor 和 InitializingBean ,这里实现第二个接口的作用主要是校验有没有配置 待扫描包的路径 :
public void afterPropertiesSet() throws Exception { notNull(this.basePackage, "Property 'basePackage' is required"); }
主要看到 postProcessBeanDefinitionRegistry 方法:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(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容器中去:
public int scan(String... basePackages) { int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); doScan(basePackages); // Register annotation config processors, if necessary. if (this.includeAnnotationConfig) { AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); } return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); } public Set<BeanDefinitionHolder> doScan(String... basePackages) { Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) { logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration."); } else { processBeanDefinitions(beanDefinitions); } return beanDefinitions; } private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); if (logger.isDebugEnabled()) { logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface"); } // the mapper interface is the original class of the bean // but, the actual class of the bean is MapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); boolean explicitFactoryUsed = false; if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) { definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionFactory != null) { definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory); explicitFactoryUsed = true; } if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionTemplate != null) { if (explicitFactoryUsed) { logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored."); } definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); explicitFactoryUsed = true; } if (!explicitFactoryUsed) { if (logger.isDebugEnabled()) { logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); } definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
你可能会好奇,在哪里生成的代理对象?只是将Mapper接口注入到IOC有什么用呢?其实关键代码就在 definition.setBeanClass(this.mapperFactoryBean.getClass()) ,这句代码的作用就是将每一个Mapper接口都转为 MapperFactoryBean 类型。
为什么要这么转呢?进入这个类你会发现它也是实现了 FactoryBean 接口的,所以自然而然的又是利用它来创建代理实现类对象:
public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); }
3. 如何整合Spring事务
Mybatis作为一个ORM框架,它是有自己的数据源和事务控制的,而Spring同样也会配置这两个,那么怎么将它们整合到一起呢?而不是在Service类调用Mapper接口时就切换了数据源和连接,那样肯定是不行的。
在使用Mybatis时,我们可以在xml中配置 TransactionFactory 事务工厂类,不过一般都会使用默认的 JdbcTransactionFactory ,而当与Spring整合后,默认的事务工厂类改为了 SpringManagedTransactionFactory 。回到 SqlSessionFactoryBean 读取配置的方法,在该方法中有下面这样一段代码:
if (this.transactionFactory == null) { this.transactionFactory = new SpringManagedTransactionFactory(); } configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
上面默认创建了 SpringManagedTransactionFactory ,同时还将我们xml中 ref 属性引用的 dataSource 添加到了 Configuration 中,这个工厂会创建下面这个事务控制对象:
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) { return new SpringManagedTransaction(dataSource); }
而这个方法是在 DefaultSqlSessionFactory 获取 SqlSession 时会调用:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
这就保证使用的是同一个数据源对象,但是怎么保证拿到的是同一个连接和事务呢?关键就在于 SpringManagedTransaction 获取连接是怎么实现的:
public Connection getConnection() throws SQLException { if (this.connection == null) { openConnection(); } return this.connection; } private void openConnection() throws SQLException { this.connection = DataSourceUtils.getConnection(this.dataSource); this.autoCommit = this.connection.getAutoCommit(); this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring"); } }
这里委托给了 DataSourceUtils 获取连接:
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { try { return doGetConnection(dataSource); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex); } } public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } return conHolder.getConnection(); } // Else we either got no holder or an empty thread-bound holder here. logger.debug("Fetching JDBC Connection from DataSource"); Connection con = dataSource.getConnection(); if (TransactionSynchronizationManager.isSynchronizationActive()) { logger.debug("Registering transaction synchronization for JDBC Connection"); // Use same Connection for further JDBC actions within the transaction. // Thread-bound object will get removed by synchronization at transaction completion. ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else { holderToUse.setConnection(con); } holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } return con; }
看到 ConnectionHolder conHolder = (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进行扩展。
以上所述就是小编给大家介绍的《Mybatis插件扩展以及与Spring整合原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- GIMP 2.10.6 发布,即将支持扩展插件
- 同步你 VSCode 设置及扩展插件,换机不用愁
- Chrome 扩展插件数次审核被拒的惨痛经历
- 【RPA插件开发】使用 Lua 扩展 UiBot 的功能
- Edge 浏览器新特性:API 升级、扩展插件改善
- 火狐浏览器66将减少内存占用,扩展插件性能加强
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML 压缩/解压工具
在线压缩/解压 HTML 代码
URL 编码/解码
URL 编码/解码