1.mybatis插件机制源码解析
2.Pagehelper的源码reasonable配置引起分页失效问题
3.一文搞懂Mybatis插件原理
4.多年以后PageHelper又深深的给我上了一课!!源码
5.mybatis分页插件pagehelper工作原理和配置过程是源码什么?
6.MyBatis分页插件PageHelper自定义分页逻辑实现
mybatis插件机制源码解析
引言
本篇源码解析基于MyBatis3.5.8版本。
首先需要说明的源码是,本篇文章不是源码mybatis插件开发的教程,而是源码cg源码从源码层面分析mybatis是如何支持用户自定义插件开发的。
mybatis的源码插件机制,让其扩展能力大大增加。源码比如我们项目中经常用到的源码PageHelper,这就是源码一款基于mybatis插件能力开发的产品,它的源码功能是让基于mybatis的数据库分页查询更容易使用。
当然基于插件我们还可以开发其它功能,源码比如在执行sql前打印日志、源码做权限控制等。源码
正文mybatis插件也叫mybatis拦截器,源码它支持从方法级别对mybatis进行拦截。整体架构图如下:
解释下几个相关概念:
Interceptor拦截器接口,用户自定义的拦截器就是实现该接口。
InterceptorChain拦截器链,其内部维护一个interceptorslist,表示拦截器链中所有的拦截器,并提供增加或获取拦截器链的方法。比如有个核心的方法是pluginAll。该方法用来生成代理对象。
Invocation拦截器执行时的上下文环境,其实就是目标方法的调用信息,包含目标对象、调用的方法信息、参数信息。核心方法是proceed。该方法的主要目的就是进行处理链的传播,执行完拦截器的方法后,最终需要调用目标方法的invoke方法。
mybatis支持在哪些地方进行拦截呢?你只需要在代码里搜索interceptorChain.pluginAll的使用位置就可以获取答案,一共有四处:
parameterHandler=(ParameterHandler)interceptorChain.pluginAll(parameterHandler);resultSetHandler=(ResultSetHandler)interceptorChain.pluginAll(resultSetHandler);statementHandler=(StatementHandler)interceptorChain.pluginAll(statementHandler);executor=(Executor)interceptorChain.pluginAll(executor);这四处实现的原理都是一样的,我们只需要选择一个进行分析就可以了。
我们先来看下自定义的插件是如何加载进来的,比如我们使用PageHelper插件,通常会在mybatis-config.xml中加入如下的配置:
<plugins><plugininterceptor="com.github.pagehelper.PageInterceptor"><!--configparamsasthefollowing--><propertyname="param1"value="value1"/></plugin></plugins>mybatis在创建SqlSessionFactory的时候会加载配置文件,
publicConfigurationparse(){ if(parsed){ thrownewBuilderException("EachXMLConfigBuildercanonlybeusedonce.");}parsed=true;parseConfiguration(parser.evalNode("/configuration"));returnconfiguration;}parseConfiguration方法会加载包括plugins在内的很多配置,
privatevoidparseConfiguration(XNoderoot){ try{ ...pluginElement(root.evalNode("plugins"));...}catch(Exceptione){ thrownewBuilderException("ErrorparsingSQLMapperConfiguration.Cause:"+e,scrmphp源码e);}}privatevoidpluginElement(XNodeparent)throwsException{ if(parent!=null){ for(XNodechild:parent.getChildren()){ Stringinterceptor=child.getStringAttribute("interceptor");Propertiesproperties=child.getChildrenAsProperties();InterceptorinterceptorInstance=(Interceptor)resolveClass(interceptor).getDeclaredConstructor().newInstance();interceptorInstance.setProperties(properties);configuration.addInterceptor(interceptorInstance);}}}pluginElement干了几件事情:
创建Interceptor实例
设置实例的属性变量
添加到Configuration的interceptorChain拦截器链中
mybatis的插件是通过动态代理实现的,那肯定要生成代理对象,生成的逻辑就是前面提到的pluginAll方法,比如对于Executor生成代理对象就是,
executor=(Executor)interceptorChain.pluginAll(executor);接着看pluginAll方法,
/***该方法会遍历用户定义的插件实现类(Interceptor),并调用Interceptor的plugin方法,对target进行插件化处理,*即我们在实现自定义的Interceptor方法时,在plugin中需要根据自己的逻辑,对目标对象进行包装(代理),创建代理对象,*那我们就可以在该方法中使用Plugin#wrap来创建代理类。*/publicObjectpluginAll(Objecttarget){ for(Interceptorinterceptor:interceptors){ target=interceptor.plugin(target);}returntarget;}这里遍历所有我们定义的拦截器,调用拦截器的plugin方法生成代理对象。有人可能有疑问:如果有多个拦截器,target不是被覆盖了吗?
其实不会,所以如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条,执行的时候,依次执行所有拦截器的拦截逻辑代码。
plugin方法是接口Interceptor的默认实现类,
defaultObjectplugin(Objecttarget){ returnPlugin.wrap(target,this);}然后进入org.apache.ibatis.plugin.Plugin#wrap,
publicstaticObjectwrap(Objecttarget,Interceptorinterceptor){ Map<Class<?>,Set<Method>>signatureMap=getSignatureMap(interceptor);Class<?>type=target.getClass();Class<?>[]interfaces=getAllInterfaces(type,signatureMap);if(interfaces.length>0){ returnProxy.newProxyInstance(type.getClassLoader(),interfaces,newPlugin(target,interceptor,signatureMap));}returntarget;}首先是获取我们自己实现的Interceptor的方法签名映射表。然后获取需要代理的对象的Class上声明的所有接口。比如如果我们wrap的是Executor,就是Executor的所有接口。然后就是最关键的一步,用Proxy类创建一个代理对象(newProxyInstance)。
注意,newProxyInstance方法的第三个参数,接收的是一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。
我们这里传入的是Plugin类,故在动态运行过程中会执行Plugin的invoker方法。
如果对这一段不是很理解,建议先了解下java动态代理的原理。java动态代理机制中有两个重要的mini源码角色:InvocationHandler(接口)和Proxy(类),这个是背景知识需要掌握的。
我们在深入看下上面的getSignatureMap方法,
privatestaticMap<Class<?>,Set<Method>>getSignatureMap(Interceptorinterceptor){ //从Interceptor的类上获取Intercepts注解,说明我们自定义拦截器需要带注解InterceptsinterceptsAnnotation=interceptor.getClass().getAnnotation(Intercepts.class);//issue#if(interceptsAnnotation==null){ thrownewPluginException("No@Interceptsannotationwasfoundininterceptor"+interceptor.getClass().getName());}Signature[]sigs=interceptsAnnotation.value();Map<Class<?>,Set<Method>>signatureMap=newHashMap<>();//解析Interceptor的values属性(Signature[])数组,存入HashMap,Set<Method>>for(Signaturesig:sigs){ Set<Method>methods=MapUtil.computeIfAbsent(signatureMap,sig.type(),k->newHashSet<>());try{ Methodmethod=sig.type().getMethod(sig.method(),sig.args());methods.add(method);}catch(NoSuchMethodExceptione){ thrownewPluginException("Couldnotfindmethodon"+sig.type()+"named"+sig.method()+".Cause:"+e,e);}}returnsignatureMap;}首先需要从Interceptor的类上获取Intercepts注解,说明我们自定义拦截器需要带注解,比如PageHelper插件的定义如下:
<plugins><plugininterceptor="com.github.pagehelper.PageInterceptor"><!--configparamsasthefollowing--><propertyname="param1"value="value1"/></plugin></plugins>0所以我们可以知道,getSignatureMap其实就是拿到我们自定义拦截器声明需要拦截的类以及类对应的方法。
前面说过,当我们调用代理对象时,最终会执行Plugin类的invoker方法,我们看下Plugin的invoker方法,
<plugins><plugininterceptor="com.github.pagehelper.PageInterceptor"><!--configparamsasthefollowing--><propertyname="param1"value="value1"/></plugin></plugins>1Interceptor接口的intercept方法就是我们自定义拦截器需要实现的逻辑,其参数为Invocation,可从Invocation参数中拿到执行方法的对象,方法,方法参数,比如我们可以从statementHandler拿到SQL语句,实现自己的特殊逻辑。
在该方法的结束需要调用invocation#proceed()方法,进行拦截器链的传播。
参考:
blogs.com/chenpi/p/.html
Pagehelper的reasonable配置引起分页失效问题
某日,一个意外的接口请求激发了我对分页机制的好奇心。前端请求一个列表,却遇到了两个出乎意料的问题。第一,get请求包含pageNum和pageSize参数,数据库执行的是常规的查询语句,并未使用limit,也未配置分页相关的bean或mybatis拦截器,却能实现分页效果。第二,请求时传入分页参数,结果却总是返回第一页的数据。
为了揭开这个谜团,我开始了细致的源码分析。通过一系列的排查,最终找到了问题的根源。原来,syncing源码引入的pagehelper插件在SpringBoot中自动配置了分页功能,无需额外的mybatis拦截器配置和PageHelper.startPage(pageNum, pageSize)的调用。这得益于SpringBoot的自动装配特性。在使用pagehelper时,通常需要在执行SQL前手动调用startPage方法,但在这个案例中,这个步骤被自动完成了。问题的关键在于,page对象在初始化时就已经赋值,而这个值是由@EnablePage注解在全局范围内设置的,这个注解会在项目启动时自动执行。这样,即使在Controller层使用了pageNum和pageSize参数,页面也会返回第一页的数据。
接着,第二个问题浮现。当请求的页码超出实际存在的页数时,页面总是返回第一页的数据。深入分析后发现,问题出在分页插件的拦截器中,这个拦截器在计算总数时,对pageNum进行了重新计算,导致最终返回的是第一页的数据。这个行为是为了防止分页不合理的情况,如页码过大或为负数,会默认返回第一页。然而,这个机制默认为合理化计算,即reasonable属性为true,导致了问题的出现。解决方法是将reasonable属性设置为false,或者在调用PageHelper.startPage时传入false作为参数,以避免自动计算。另外,不配置合理化参数也是一个选择,因为其默认值为false。
通过这次深入的源码分析,我不仅解决了眼前的两个问题,还对PageHelper和mybatis的propertylist源码分页机制有了更深入的理解。同时,我也意识到分页合理化参数的使用需要谨慎考虑,因为它可能会导致某些情况下的死循环问题,尤其是在批量导出数据时,若数据量大,一次性全读取可能会陷入死循环。因此,个人建议在实际应用中,使用默认配置或明确设置为false,以避免潜在的逻辑问题。
一文搞懂Mybatis插件原理
Mybatis插件原理详解,让你轻松实现自定义功能
在深入Mybatis源码时,你可能会遇到名为"plugin"的包,这实际上用于扩展框架的功能,如PageHelper分页插件。本文将揭示插件的运作机制,教你如何理解其内部工作并自行实现,如分页或慢SQL统计。
使用PageHelper插件分页的简单步骤是:一,在pom.xml中添加依赖;二,在mybatis-config.xml中配置插件;三,在需要分页的mapper接口前调用startPage方法,并确保每次查询后关闭Page对象以避免数据冲突。
接下来,我们将探索插件的总体流程:首先,通过拦截器接口实现动态代理,如PageInterceptor,它会在Executor的query方法执行前后添加自定义逻辑。拦截器通过Invocation对象获取目标对象和方法参数,实现增强功能。
分页拦截器如PageInterceptor拦截query方法,若需要分页,会获取原始SQL并添加limit语句。配置插件时,在mybatis-config.xml中添加相应拦截器,如``标签和Interceptor实现类的全限定名。
当我们创建SqlSession时,拦截器通过Configuration的newExecutor方法注册并应用到Executor对象上,利用JDK动态代理生成增强的代理对象,从而在执行SQL时调用拦截器的intercept方法。
自定义慢SQL统计插件同样基于Interceptor,只需配置目标对象为StatementHandler,拦截方法为query、update或batch,然后在配置文件中添加插件的全限定名。
多年以后PageHelper又深深的给我上了一课!!
多年未涉足PageHelper,新项目采用集成框架,开发顺利,却在最终测试中遇到了深刻的教学。接下来,我将分享我遇到的几个独特现象。
账号重复注册?
这是指已注册的账号居然能再次注册成功。问题出在`checkUserNameUnique(username)`,正常逻辑下不应出现此现象。问题所在,我们随后解答。
查询全部分类的下拉列表为何只能查出5条数据?
明明有十多个结果,为何只返回5个?未配置分页参数,是PageHelper的分页机制在起作用。
修改用户密码时,为何报错?
后台界面重置用户密码时,出现SQL语句异常,原因在于SQL语句中拼接了“limit”参数。
PageHelper如何导致以上问题?
PageHelper使用方式简单,通过`startPage()`指定分页参数,`getDataTable(list)`封装分页格式。
为何未传分页参数?原因是PageHelper内部机制独特,通过ThreadLocal存储配置,自动处理分页。
ThreadLocal如何造成问题?
ThreadLocal存储了分页参数,若未在使用后清除,下一次请求时会使用旧配置,导致异常。
在MyBatis使用PageHelper时,何时获取ThreadLocal?
PageHelper作为分页插件,通过拦截器`PageInterceptor`实现分页效果,其核心在于`intercept`方法中的逻辑。
如何设置分页?
调用`dialect.skip(ms, parameter, rowBounds)`设置分页参数,`getPage()`获取ThreadLocal值,决定分页操作。
分页逻辑包含哪些步骤?
先统计数量,若为0直接返回,然后进行分页,最后处理分页结果。
为何非分页操作也影响?
调用`ExecutorUtil.pageQuery`分页逻辑,即使不分页也会执行,导致SQL语句异常。
PageHelper如何清理ThreadLocal?
在`intercept`方法结束后,调用`afterAll`方法清除缓存。
如何避免问题?
确保在执行SQL代码后调用`startPage()`,或在存在问题的方法前手动调用`clearPage()`。
为何不是每次请求都出错?
容器如Tomcat通过线程池复用线程处理请求,若线程持有未清除的分页参数,会导致问题。
总结
PageHelper的使用涉及ThreadLocal的巧妙利用,但也带来问题。通过理解其原理和机制,我们能有效避免和解决此类问题。尽管它带来挑战,但深入研究也加深了对MyBatis和PageHelper实现的理解,对于热衷源码阅读的开发者而言,这是一次宝贵的学习经历。
mybatis分页插件pagehelper工作原理和配置过程是什么?
PageHelper,一款为MyBatis框架量身打造的强大分页插件,适用于包括MySQL、Oracle、MariaDB、SQLite、Hsqldb在内的多种主流数据库。它提供了一种便捷的方式来实现分页,通过在原始SQL查询语句前添加`PageHelper.startPage(pageNum, pageSize);`,启动分页功能。在执行查询后,通过`PageInfo`对象可以获取到分页信息,如总记录数、总页数、每页大小等。
PageHelper的实现原理基于拦截器(Interceptor),在执行相关SQL之前会进行拦截并进行分页处理。通过ThreadLocal机制,将分页参数安全地保存在当前线程中,确保了分页参数的准确性和安全性。此外,PageHelper提供了丰富的配置选项和自定义功能,以满足不同场景下的需求,如支持带有“for update”的查询语句、支持嵌套查询等。
总的来说,PageHelper是一个功能强大且易于使用的分页插件,它简化了MyBatis框架中的分页处理,提高了开发效率。
集成PageHelper到项目中非常简单。首先,需要引入PageHelper的依赖。如果项目中已经引入了mybatis-plus,为避免依赖冲突,建议使用PageHelper的特定版本,这里使用的是1.4.7版本。获取更多关于PageHelper的依赖信息,可以访问官方仓库mvnrepository.com进行查找。
在使用PageHelper时,需要在Mapper.xml中实现SQL语句,并在执行查询前调用`PageHelper.startPage()`方法,传入所需的分页参数。在查询执行后,返回的分页信息通过`PageInfo`对象获取,这使得分页处理变得直观且高效。
PageHelper的源码解析涉及其工作原理、配置与实现细节。PageHelper的工作原理基于拦截器,通过ThreadLocal机制存储分页参数,确保了分页操作在安全的线程上下文中进行。启动分页的`PageHelper.startPage()`方法在当前线程中设置分页参数,后续执行的SQL查询将自动进行分页处理。在查询执行完毕后,PageHelper会清除线程中的分页参数,以避免资源泄露。
PageHelper的配置与使用涉及其内部实现,如初始化、拦截器注册等。通过自动装配配置,PageHelper能够在Spring Boot环境中无缝集成,自动注册分页拦截器到MyBatis配置中。执行查询时,MyBatis会通过动态代理方式添加分页插件,实现SQL语句的动态拼接,生成带有分页参数的SQL查询,从而实现实现分页功能。
在处理分页语句时,PageHelper通过`getPageSql()`方法实现SQL的拼接过程。该方法在获取分页信息后,根据数据库方言生成带有`LIMIT`和`OFFSET`的分页SQL语句。最终,生成的分页SQL语句会被执行,返回分页结果。
PageHelper的源码解析提供了对分页插件内部机制的深入了解,对于理解和优化分页处理有重要意义。对于追求性能优化和深入理解框架实现细节的开发者来说,这些解析内容尤其宝贵。
最后,本文提供了一些学习资源和福利,包括Java、算法、数据库、多线程等技术的学习笔记,以及最新大厂面试题集、项目源码等,旨在帮助开发者提升技能、拓展视野。
MyBatis分页插件PageHelper自定义分页逻辑实现
PageHelper在MySQL中使用limit子句进行分页,在小数据量下表现良好,但面对大数据量,效率较低,因为MySQL需先查询所有数据再过滤。为优化,我曾分享过《MySQL查询优化》中的自关联方法,对于每页条数据的场景,即使不使用索引,效率也可接受。
深入研究PageHelper源码后,我发现其分页逻辑主要在AbstractHelperDialect类中,通过子类实现如MySqlDialect。MySQL的分页实现是模板方法模式,通过覆写getPageSql方法来改变分页逻辑。我决定自定义一个Dialect类,继承MySqlDialect,重写getPageSql方法以实现自定义需求。
在配置阶段,由于缺乏获取SQL各部分源码的工具,我选择正则表达式处理。我之前的文章《Java中的正则表达式概述》可能对你有所帮助。自定义分页逻辑应用于XML SQL,如对user表分页,使用方法与原版PageHelper类似。
以下是我自定义分页逻辑的代码演示和执行效果,可以看到成功实现了主键为id的表的分页。同时,支持主键名非id的情况,通过Spring AOP的自定义注解处理。
虽然目前仅支持单表MySQL分页,不涉及多表连接,但未来可能考虑通过视图View来扩展。对于不使用JOIN的笛卡尔积查询,暂存有bug,可通过设置isRelegated为true降级为原版逻辑,以保证正确性但牺牲部分性能。