• 其实MyBatis的插件机制可以帮我们解决工作很多问题,建议收藏


    MyBatis插件

      插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或者改变原有的功能,MyBatis中也提供的有插件,虽然叫插件,但是实际上是通过拦截器(Interceptor)实现的,在MyBatis的插件模块中涉及到责任链模式和JDK动态代理,这两种设计模式的技术知识也是大家要提前掌握的。

    1. 自定义插件

      首先我们来看下一个自定义的插件我们要如何来实现。

    1.1 创建Interceptor实现类

      我们创建的拦截器必须要实现Interceptor接口,Interceptor接口的定义为

    1. public interface Interceptor {
    2. // 执行拦截逻辑的方法
    3. Object intercept(Invocation invocation) throws Throwable;
    4. // 决定是否触发 intercept()方法
    5. default Object plugin(Object target) {
    6. return Plugin.wrap(target, this);
    7. }
    8. // 根据配置 初始化 Intercept 对象
    9. default void setProperties(Properties properties) {
    10. // NOP
    11. }
    12. }

    在MyBatis中Interceptor允许拦截的内容是

    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    • ParameterHandler (getParameterObject, setParameters)
    • ResultSetHandler (handleResultSets, handleOutputParameters)
    • StatementHandler (prepare, parameterize, batch, update, query)

      我们创建一个拦截Executor中的query和close的方法

    1. package com.gupaoedu.interceptor;
    2. import org.apache.ibatis.executor.Executor;
    3. import org.apache.ibatis.mapping.MappedStatement;
    4. import org.apache.ibatis.plugin.*;
    5. import org.apache.ibatis.session.ResultHandler;
    6. import org.apache.ibatis.session.RowBounds;
    7. import java.util.Properties;
    8. /**
    9. * 自定义的拦截器
    10. * @Signature 注解就可以表示一个方法签名, 唯一确定一个方法
    11. */
    12. @Intercepts({
    13. @Signature(
    14. type = Executor.class // 需要拦截的类型
    15. ,method = "query" // 需要拦截的方法
    16. // args 中指定 被拦截方法的 参数列表
    17. ,args={MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class}
    18. ),
    19. @Signature(
    20. type = Executor.class
    21. ,method = "close"
    22. ,args = {boolean.class}
    23. )
    24. })
    25. public class FirstInterceptor implements Interceptor {
    26. private int testProp;
    27. /**
    28. * 执行拦截逻辑的方法
    29. * @param invocation
    30. * @return
    31. * @throws Throwable
    32. */
    33. @Override
    34. public Object intercept(Invocation invocation) throws Throwable {
    35. System.out.println("FirtInterceptor 拦截之前 ....");
    36. Object obj = invocation.proceed();
    37. System.out.println("FirtInterceptor 拦截之后 ....");
    38. return obj;
    39. }
    40. /**
    41. * 决定是否触发 intercept方法
    42. * @param target
    43. * @return
    44. */
    45. @Override
    46. public Object plugin(Object target) {
    47. return Plugin.wrap(target,this);
    48. }
    49. @Override
    50. public void setProperties(Properties properties) {
    51. System.out.println("---->"+properties.get("testProp"));
    52. }
    53. public int getTestProp() {
    54. return testProp;
    55. }
    56. public void setTestProp(int testProp) {
    57. this.testProp = testProp;
    58. }
    59. }

    1.2 配置拦截器

      创建好自定义的拦截器后,我们需要在全局配置文件中添加自定义插件的注册

    1. <plugins>
    2. <plugin interceptor="com.gupaoedu.interceptor.FirstInterceptor">
    3. <property name="testProp" value="1000"/>
    4. </plugin>
    5. </plugins>

    1.3 运行程序

      然后我们执行对应的查询操作。

    1. @Test
    2. public void test1() throws Exception{
    3. // 1.获取配置文件
    4. InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
    5. // 2.加载解析配置文件并获取SqlSessionFactory对象
    6. SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
    7. // 3.根据SqlSessionFactory对象获取SqlSession对象
    8. SqlSession sqlSession = factory.openSession();
    9. // 4.通过SqlSession中提供的 API方法来操作数据库
    10. List<User> list = sqlSession.selectList("com.gupaoedu.mapper.UserMapper.selectUserList");
    11. for (User user : list) {
    12. System.out.println(user);
    13. }
    14. // 5.关闭会话
    15. sqlSession.close();
    16. }

    在输出语句中可以看到我们的日志信息

    拦截的query方法和close方法的源码位置在如下: 

     

    2. 插件实现原理

      自定义插件的步骤还是比较简单的,接下来我们分析下插件的实现原理是怎么回事。

    2.1 初始化操作

      首先我们来看下在全局配置文件加载解析的时候做了什么操作。

     

    进入方法内部可以看到具体的解析操作 

    1. private void pluginElement(XNode parent) throws Exception {
    2. if (parent != null) {
    3. for (XNode child : parent.getChildren()) {
    4. // 获取<plugin> 节点的 interceptor 属性的值
    5. String interceptor = child.getStringAttribute("interceptor");
    6. // 获取<plugin> 下的所有的properties子节点
    7. Properties properties = child.getChildrenAsProperties();
    8. // 获取 Interceptor 对象
    9. Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
    10. // 设置 interceptor的 属性
    11. interceptorInstance.setProperties(properties);
    12. // Configuration中记录 Interceptor
    13. configuration.addInterceptor(interceptorInstance);
    14. }
    15. }
    16. }

    该方法用来解析全局配置文件中的plugins标签,然后对应的创建Interceptor对象,并且封装对应的属性信息。最后调用了Configuration对象中的方法。
    configuration.addInterceptor(interceptorInstance)

    1. public void addInterceptor(Interceptor interceptor) {
    2. interceptorChain.addInterceptor(interceptor);
    3. }

    通过这个代码我们发现我们自定义的拦截器最终是保存在了InterceptorChain这个对象中。而InterceptorChain的定义为

    1. public class InterceptorChain {
    2. // 保存所有的 Interceptor 也就我所有的插件是保存在 Interceptors 这个List集合中的
    3. private final List<Interceptor> interceptors = new ArrayList<>();
    4. //
    5. public Object pluginAll(Object target) {
    6. for (Interceptor interceptor : interceptors) { // 获取拦截器链中的所有拦截器
    7. target = interceptor.plugin(target); // 创建对应的拦截器的代理对象
    8. }
    9. return target;
    10. }
    11. public void addInterceptor(Interceptor interceptor) {
    12. interceptors.add(interceptor);
    13. }
    14. public List<Interceptor> getInterceptors() {
    15. return Collections.unmodifiableList(interceptors);
    16. }
    17. }

    2.2 如何创建代理对象

      在解析的时候创建了对应的Interceptor对象,并保存在了InterceptorChain中,那么这个拦截器是如何和对应的目标对象进行关联的呢?首先拦截器可以拦截的对象是 Executor,ParameterHandler,ResultSetHandler,StatementHandler.那么我们来看下这四个对象在创建的时候又什么要注意的

    2.2.1 Executor

    我们可以看到Executor在装饰完二级缓存后会通过pluginAll来创建Executor的代理对象。 

    1. public Object pluginAll(Object target) {
    2. for (Interceptor interceptor : interceptors) { // 获取拦截器链中的所有拦截器
    3. target = interceptor.plugin(target); // 创建对应的拦截器的代理对象
    4. }
    5. return target;
    6. }

    进入plugin方法中,我们会进入到

    1. // 决定是否触发 intercept()方法
    2. default Object plugin(Object target) {
    3. return Plugin.wrap(target, this);
    4. }

     然后进入到MyBatis给我们提供的Plugin工具类的实现 wrap方法中。

    1. /**
    2. * 创建目标对象的代理对象
    3. * 目标对象 Executor ParameterHandler ResultSetHandler StatementHandler
    4. * @param target 目标对象
    5. * @param interceptor 拦截器
    6. * @return
    7. */
    8. public static Object wrap(Object target, Interceptor interceptor) {
    9. // 获取用户自定义 Interceptor中@Signature注解的信息
    10. // getSignatureMap 负责处理@Signature 注解
    11. Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    12. // 获取目标类型
    13. Class<?> type = target.getClass();
    14. // 获取目标类型 实现的所有的接口
    15. Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    16. // 如果目标类型有实现的接口 就创建代理对象
    17. if (interfaces.length > 0) {
    18. return Proxy.newProxyInstance(
    19. type.getClassLoader(),
    20. interfaces,
    21. new Plugin(target, interceptor, signatureMap));
    22. }
    23. // 否则原封不动的返回目标对象
    24. return target;
    25. }

     Plugin中的各个方法的作用

    1. public class Plugin implements InvocationHandler {
    2. private final Object target; // 目标对象
    3. private final Interceptor interceptor; // 拦截器
    4. private final Map<Class<?>, Set<Method>> signatureMap; // 记录 @Signature 注解的信息
    5. private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    6. this.target = target;
    7. this.interceptor = interceptor;
    8. this.signatureMap = signatureMap;
    9. }
    10. /**
    11. * 创建目标对象的代理对象
    12. * 目标对象 Executor ParameterHandler ResultSetHandler StatementHandler
    13. * @param target 目标对象
    14. * @param interceptor 拦截器
    15. * @return
    16. */
    17. public static Object wrap(Object target, Interceptor interceptor) {
    18. // 获取用户自定义 Interceptor中@Signature注解的信息
    19. // getSignatureMap 负责处理@Signature 注解
    20. Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    21. // 获取目标类型
    22. Class<?> type = target.getClass();
    23. // 获取目标类型 实现的所有的接口
    24. Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    25. // 如果目标类型有实现的接口 就创建代理对象
    26. if (interfaces.length > 0) {
    27. return Proxy.newProxyInstance(
    28. type.getClassLoader(),
    29. interfaces,
    30. new Plugin(target, interceptor, signatureMap));
    31. }
    32. // 否则原封不动的返回目标对象
    33. return target;
    34. }
    35. /**
    36. * 代理对象方法被调用时执行的代码
    37. * @param proxy
    38. * @param method
    39. * @param args
    40. * @return
    41. * @throws Throwable
    42. */
    43. @Override
    44. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    45. try {
    46. // 获取当前方法所在类或接口中,可被当前Interceptor拦截的方法
    47. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    48. if (methods != null && methods.contains(method)) {
    49. // 当前调用的方法需要被拦截 执行拦截操作
    50. return interceptor.intercept(new Invocation(target, method, args));
    51. }
    52. // 不需要拦截 则调用 目标对象中的方法
    53. return method.invoke(target, args);
    54. } catch (Exception e) {
    55. throw ExceptionUtil.unwrapThrowable(e);
    56. }
    57. }
    58. private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    59. Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    60. // issue #251
    61. if (interceptsAnnotation == null) {
    62. throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    63. }
    64. Signature[] sigs = interceptsAnnotation.value();
    65. Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    66. for (Signature sig : sigs) {
    67. Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
    68. try {
    69. Method method = sig.type().getMethod(sig.method(), sig.args());
    70. methods.add(method);
    71. } catch (NoSuchMethodException e) {
    72. throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
    73. }
    74. }
    75. return signatureMap;
    76. }
    77. private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    78. Set<Class<?>> interfaces = new HashSet<>();
    79. while (type != null) {
    80. for (Class<?> c : type.getInterfaces()) {
    81. if (signatureMap.containsKey(c)) {
    82. interfaces.add(c);
    83. }
    84. }
    85. type = type.getSuperclass();
    86. }
    87. return interfaces.toArray(new Class<?>[interfaces.size()]);
    88. }
    89. }

    2.2.2 StatementHandler

      在获取StatementHandler的方法中

    1. @Override
    2. public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    3. Statement stmt = null;
    4. try {
    5. Configuration configuration = ms.getConfiguration();
    6. // 注意,已经来到SQL处理的关键对象 StatementHandler >>
    7. StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    8. // 获取一个 Statement对象
    9. stmt = prepareStatement(handler, ms.getStatementLog());
    10. // 执行查询
    11. return handler.query(stmt, resultHandler);
    12. } finally {
    13. // 用完就关闭
    14. closeStatement(stmt);
    15. }
    16. }

    在进入newStatementHandler方法

    1. public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    2. StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    3. // 植入插件逻辑(返回代理对象)
    4. statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    5. return statementHandler;
    6. }

    可以看到statementHandler的代理对象

    2.2.3 ParameterHandler

      在上面步骤的RoutingStatementHandler方法中,我们来看看

    1. public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    2. // StatementType 是怎么来的? 增删改查标签中的 statementType="PREPARED",默认值 PREPARED
    3. switch (ms.getStatementType()) {
    4. case STATEMENT:
    5. delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    6. break;
    7. case PREPARED:
    8. // 创建 StatementHandler 的时候做了什么? >>
    9. delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    10. break;
    11. case CALLABLE:
    12. delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    13. break;
    14. default:
    15. throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    16. }
    17. }

     然后我们随便选择一个分支进入,比如PreparedStatementHandler

    在newParameterHandler的步骤我们可以发现代理对象的创建

    1. public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    2. ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    3. // 植入插件逻辑(返回代理对象)
    4. parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    5. return parameterHandler;
    6. }

    2.2.4 ResultSetHandler

      在上面的newResultSetHandler()方法中,也可以看到ResultSetHander的代理对象

    1. public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
    2. ResultHandler resultHandler, BoundSql boundSql) {
    3. ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    4. // 植入插件逻辑(返回代理对象)
    5. resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    6. return resultSetHandler;
    7. }

    2.3 执行流程

       以Executor的query方法为例,当查询请求到来的时候,Executor的代理对象是如何处理拦截请求的呢?我们来看下。当请求到了executor.query方法的时候

     

    然后会执行Plugin的invoke方法

    1. /**
    2. * 代理对象方法被调用时执行的代码
    3. * @param proxy
    4. * @param method
    5. * @param args
    6. * @return
    7. * @throws Throwable
    8. */
    9. @Override
    10. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    11. try {
    12. // 获取当前方法所在类或接口中,可被当前Interceptor拦截的方法
    13. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    14. if (methods != null && methods.contains(method)) {
    15. // 当前调用的方法需要被拦截 执行拦截操作
    16. return interceptor.intercept(new Invocation(target, method, args));
    17. }
    18. // 不需要拦截 则调用 目标对象中的方法
    19. return method.invoke(target, args);
    20. } catch (Exception e) {
    21. throw ExceptionUtil.unwrapThrowable(e);
    22. }
    23. }

    然后进入interceptor.intercept 会进入我们自定义的 FirstInterceptor对象中

    1. /**
    2. * 执行拦截逻辑的方法
    3. * @param invocation
    4. * @return
    5. * @throws Throwable
    6. */
    7. @Override
    8. public Object intercept(Invocation invocation) throws Throwable {
    9. System.out.println("FirtInterceptor 拦截之前 ....");
    10. Object obj = invocation.proceed(); // 执行目标方法
    11. System.out.println("FirtInterceptor 拦截之后 ....");
    12. return obj;
    13. }

    这个就是自定义的拦截器执行的完整流程,

    2.4 多拦截器

      如果我们有多个自定义的拦截器,那么他的执行流程是怎么样的呢?比如我们创建了两个 Interceptor 都是用来拦截 Executor 的query方法,一个是用来执行逻辑A 一个是用来执行逻辑B的。

    单个拦截器的执行流程

     

    如果说对象被代理了多次,这里会继续调用下一个插件的逻辑,再走一次Plugin的invoke()方法。这里我们需要关注一下有多个插件的时候的运行顺序。

      配置的顺序和执行的顺序是相反的。InterceptorChain的List是按照插件从上往下的顺序解析、添加的。

      而创建代理的时候也是按照list的顺序代理。执行的时候当然是从最后代理的对象开始。

     

    这个我们可以通过实际的案例来得到验证,最后来总结下Interceptor的相关对象的作用 

    3. PageHelper分析

       Mybatis的插件使用中分页插件PageHelper应该是我们使用到的比较多的插件应用。我们先来回顾下PageHelper的应用

    3.1 PageHelper的应用

      添加依赖

     

    1. <dependency>
    2. <groupId>com.github.pagehelper</groupId>
    3. <artifactId>pagehelper</artifactId>
    4. <version>4.1.6</version>
    5. </dependency>

     在全局配置文件中注册

    1. <!-- com.github.pagehelper为PageHelper类所在包名 -->
    2. <plugin interceptor="com.github.pagehelper.PageHelper">
    3. <property name="dialect" value="mysql" />
    4. <!-- 该参数默认为false -->
    5. <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
    6. <!-- 和startPage中的pageNum效果一样 -->
    7. <property name="offsetAsPageNum" value="true" />
    8. <!-- 该参数默认为false -->
    9. <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
    10. <property name="rowBoundsWithCount" value="true" />
    11. <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
    12. <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) -->
    13. <property name="pageSizeZero" value="true" />
    14. <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
    15. <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
    16. <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
    17. <property name="reasonable" value="false" />
    18. <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
    19. <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
    20. <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 -->
    21. <!-- 不理解该含义的前提下,不要随便复制该配置 -->
    22. <property name="params" value="pageNum=start;pageSize=limit;" />
    23. <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
    24. <property name="returnPageInfo" value="check" />
    25. </plugin>

    然后就是分页查询操作

    查看日志我们能够发现查询出来的只有5条记录 

    通过MyBatis的分页插件的使用,我们发现我们仅仅是在 执行操作之前设置了一句PageHelper.startPage(1,5); 并没有做其他操作,也就是没有改变任何其他的业务代码。这就是它的优点,那么我再来看下他的实现原理

    3.2 实现原理剖析

      在PageHelper中,肯定有提供Interceptor的实现类,通过源码我们可以发现是PageHelper,而且我们也可以看到在该方法头部添加的注解,声明了该拦截器拦截的是Executor的query方法

     

    然后当我们要执行查询操作的时候,我们知道 Executor.query() 方法的执行本质上是执行 Executor的代理对象的方法。先来看下Plugin中的invoke方法

    1. /**
    2. * 代理对象方法被调用时执行的代码
    3. * @param proxy
    4. * @param method
    5. * @param args
    6. * @return
    7. * @throws Throwable
    8. */
    9. @Override
    10. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    11. try {
    12. // 获取当前方法所在类或接口中,可被当前Interceptor拦截的方法
    13. Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    14. if (methods != null && methods.contains(method)) {
    15. // 当前调用的方法需要被拦截 执行拦截操作
    16. return interceptor.intercept(new Invocation(target, method, args));
    17. }
    18. // 不需要拦截 则调用 目标对象中的方法
    19. return method.invoke(target, args);
    20. } catch (Exception e) {
    21. throw ExceptionUtil.unwrapThrowable(e);
    22. }
    23. }

     interceptor.intercept(new Invocation(target, method, args));方法的执行会进入到 PageHelper的intercept方法中

    1. /**
    2. * Mybatis拦截器方法
    3. *
    4. * @param invocation 拦截器入参
    5. * @return 返回执行结果
    6. * @throws Throwable 抛出异常
    7. */
    8. public Object intercept(Invocation invocation) throws Throwable {
    9. if (autoRuntimeDialect) {
    10. SqlUtil sqlUtil = getSqlUtil(invocation);
    11. return sqlUtil.processPage(invocation);
    12. } else {
    13. if (autoDialect) {
    14. initSqlUtil(invocation);
    15. }
    16. return sqlUtil.processPage(invocation);
    17. }
    18. }

    intercept方法

    1. /**
    2. * Mybatis拦截器方法
    3. *
    4. * @param invocation 拦截器入参
    5. * @return 返回执行结果
    6. * @throws Throwable 抛出异常
    7. */
    8. public Object intercept(Invocation invocation) throws Throwable {
    9. if (autoRuntimeDialect) { // 多数据源
    10. SqlUtil sqlUtil = getSqlUtil(invocation);
    11. return sqlUtil.processPage(invocation);
    12. } else { // 单数据源
    13. if (autoDialect) {
    14. initSqlUtil(invocation);
    15. }
    16. return sqlUtil.processPage(invocation);
    17. }
    18. }

    interceptor方法中首先会获取一个SqlUtils对象

    SqlUtil:数据库类型专用sql工具类,一个数据库url对应一个SqlUtil实例,SqlUtil内有一个Parser对象,如果是mysql,它是MysqlParser,如果是oracle,它是OracleParser,这个Parser对象是SqlUtil不同实例的主要存在价值。执行count查询、设置Parser对象、执行分页查询、保存Page分页对象等功能,均由SqlUtil来完成。

    initSqlUtil 方法

    1. /**
    2. * 初始化sqlUtil
    3. *
    4. * @param invocation
    5. */
    6. public synchronized void initSqlUtil(Invocation invocation) {
    7. if (this.sqlUtil == null) {
    8. this.sqlUtil = getSqlUtil(invocation);
    9. if (!autoRuntimeDialect) {
    10. properties = null;
    11. sqlUtilConfig = null;
    12. }
    13. autoDialect = false;
    14. }
    15. }

     getSqlUtil方法

    1. /**
    2. * 根据datasource创建对应的sqlUtil
    3. *
    4. * @param invocation
    5. */
    6. public SqlUtil getSqlUtil(Invocation invocation) {
    7. MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
    8. //改为对dataSource做缓存
    9. DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
    10. String url = getUrl(dataSource);
    11. if (urlSqlUtilMap.containsKey(url)) {
    12. return urlSqlUtilMap.get(url);
    13. }
    14. try {
    15. lock.lock();
    16. if (urlSqlUtilMap.containsKey(url)) {
    17. return urlSqlUtilMap.get(url);
    18. }
    19. if (StringUtil.isEmpty(url)) {
    20. throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
    21. }
    22. String dialect = Dialect.fromJdbcUrl(url);
    23. if (dialect == null) {
    24. throw new RuntimeException("无法自动获取数据库类型,请通过dialect参数指定!");
    25. }
    26. // 创建SqlUtil
    27. SqlUtil sqlUtil = new SqlUtil(dialect);
    28. if (this.properties != null) {
    29. sqlUtil.setProperties(properties);
    30. } else if (this.sqlUtilConfig != null) {
    31. sqlUtil.setSqlUtilConfig(this.sqlUtilConfig);
    32. }
    33. urlSqlUtilMap.put(url, sqlUtil);
    34. return sqlUtil;
    35. } finally {
    36. lock.unlock();
    37. }
    38. }

    查看SqlUtil的构造方法

    1. /**
    2. * 构造方法
    3. *
    4. * @param strDialect
    5. */
    6. public SqlUtil(String strDialect) {
    7. if (strDialect == null || "".equals(strDialect)) {
    8. throw new IllegalArgumentException("Mybatis分页插件无法获取dialect参数!");
    9. }
    10. Exception exception = null;
    11. try {
    12. Dialect dialect = Dialect.of(strDialect);
    13. // 根据方言创建对应的解析器
    14. parser = AbstractParser.newParser(dialect);
    15. } catch (Exception e) {
    16. exception = e;
    17. //异常的时候尝试反射,允许自己写实现类传递进来
    18. try {
    19. Class<?> parserClass = Class.forName(strDialect);
    20. if (Parser.class.isAssignableFrom(parserClass)) {
    21. parser = (Parser) parserClass.newInstance();
    22. }
    23. } catch (ClassNotFoundException ex) {
    24. exception = ex;
    25. } catch (InstantiationException ex) {
    26. exception = ex;
    27. } catch (IllegalAccessException ex) {
    28. exception = ex;
    29. }
    30. }
    31. if (parser == null) {
    32. throw new RuntimeException(exception);
    33. }
    34. }

     创建的解析器

    1. public static Parser newParser(Dialect dialect) {
    2. Parser parser = null;
    3. switch (dialect) {
    4. case mysql:
    5. case mariadb:
    6. case sqlite:
    7. parser = new MysqlParser();
    8. break;
    9. case oracle:
    10. parser = new OracleParser();
    11. break;
    12. case hsqldb:
    13. parser = new HsqldbParser();
    14. break;
    15. case sqlserver:
    16. parser = new SqlServerParser();
    17. break;
    18. case sqlserver2012:
    19. parser = new SqlServer2012Dialect();
    20. break;
    21. case db2:
    22. parser = new Db2Parser();
    23. break;
    24. case postgresql:
    25. parser = new PostgreSQLParser();
    26. break;
    27. case informix:
    28. parser = new InformixParser();
    29. break;
    30. case h2:
    31. parser = new H2Parser();
    32. break;
    33. default:
    34. throw new RuntimeException("分页插件" + dialect + "方言错误!");
    35. }
    36. return parser;
    37. }

    我们可以看到不同的数据库方言,创建了对应的解析器。

      然后我们再回到前面的interceptor方法中继续,查看sqlUtil.processPage(invocation);方法

    1. private Object _processPage(Invocation invocation) throws Throwable {
    2. final Object[] args = invocation.getArgs();
    3. Page page = null;
    4. //支持方法参数时,会先尝试获取Page
    5. if (supportMethodsArguments) {
    6. page = getPage(args); // 有通过ThreadLocal来获取分页数据信息
    7. }
    8. //分页信息
    9. RowBounds rowBounds = (RowBounds) args[2];
    10. //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
    11. if ((supportMethodsArguments && page == null)
    12. //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
    13. || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
    14. return invocation.proceed();
    15. } else {
    16. //不支持分页参数时,page==null,这里需要获取
    17. if (!supportMethodsArguments && page == null) {
    18. page = getPage(args);
    19. }
    20. return doProcessPage(invocation, page, args);
    21. }
    22. }

     doProcessPage进入该方法

    1. private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
    2. //保存RowBounds状态
    3. RowBounds rowBounds = (RowBounds) args[2];
    4. //获取原始的ms
    5. MappedStatement ms = (MappedStatement) args[0];
    6. //判断并处理为PageSqlSource
    7. if (!isPageSqlSource(ms)) {
    8. processMappedStatement(ms);
    9. }
    10. //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
    11. ((PageSqlSource)ms.getSqlSource()).setParser(parser);
    12. try {
    13. //忽略RowBounds-否则会进行Mybatis自带的内存分页
    14. args[2] = RowBounds.DEFAULT;
    15. //如果只进行排序 或 pageSizeZero的判断
    16. if (isQueryOnly(page)) {
    17. return doQueryOnly(page, invocation);
    18. }
    19. //简单的通过total的值来判断是否进行count查询
    20. if (page.isCount()) {
    21. page.setCountSignal(Boolean.TRUE);
    22. //替换MS
    23. args[0] = msCountMap.get(ms.getId());
    24. //查询总数
    25. Object result = invocation.proceed();
    26. //还原ms
    27. args[0] = ms;
    28. //设置总数
    29. page.setTotal((Integer) ((List) result).get(0));
    30. if (page.getTotal() == 0) {
    31. return page;
    32. }
    33. } else {
    34. page.setTotal(-1l);
    35. }
    36. //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
    37. if (page.getPageSize() > 0 &&
    38. ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
    39. || rowBounds != RowBounds.DEFAULT)) {
    40. //将参数中的MappedStatement替换为新的qs
    41. page.setCountSignal(null);
    42. BoundSql boundSql = ms.getBoundSql(args[1]);
    43. // 在 invocation中绑定 分页的数据
    44. args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
    45. page.setCountSignal(Boolean.FALSE);
    46. //执行分页查询
    47. Object result = invocation.proceed();
    48. //得到处理结果
    49. page.addAll((List) result);
    50. }
    51. } finally {
    52. ((PageSqlSource)ms.getSqlSource()).removeParser();
    53. }
    54. //返回结果
    55. return page;
    56. }

    invocation.proceed();方法会进入 CachingExecutor中的query方法。

    1. @Override
    2. public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    3. // 获取SQL
    4. BoundSql boundSql = ms.getBoundSql(parameterObject);
    5. // 创建CacheKey:什么样的SQL是同一条SQL? >>
    6. CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    7. return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    8. }

    ms.getBoundSql方法中会完成分页SQL的绑定

     

    进入 PageSqlSource 的getBoundSql方法中 

    1. /**
    2. * 获取BoundSql
    3. *
    4. * @param parameterObject
    5. * @return
    6. */
    7. @Override
    8. public BoundSql getBoundSql(Object parameterObject) {
    9. Boolean count = getCount();
    10. if (count == null) {
    11. return getDefaultBoundSql(parameterObject);
    12. } else if (count) {
    13. return getCountBoundSql(parameterObject);
    14. } else {
    15. return getPageBoundSql(parameterObject);
    16. }
    17. }

     然后进入getPageBoundSql获取分页的SQL语句,在这个方法中大家也可以发现查询总的记录数的SQL生成

    1. @Override
    2. protected BoundSql getPageBoundSql(Object parameterObject) {
    3. String tempSql = sql;
    4. String orderBy = PageHelper.getOrderBy();
    5. if (orderBy != null) {
    6. tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
    7. }
    8. tempSql = localParser.get().getPageSql(tempSql);
    9. return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
    10. }

    最终在这个方法中生成了对应数据库的分页语句

     

    4.应用场景分析

      通过上面的分析应该对于MyBatis的插件原理都比较清楚了,那么在实际工作我们就可以利用这些特点来很好的解决我们的各种问题,如下(不局限于此)

     

    好了~本篇文章就介绍到这里,相信大家对于MyBatis的插件应用有了一个更加透彻的认识了,对于实际的应用应该也更加清楚了。如果对你有帮助,欢迎点赞关注加收藏

    下篇给大家介绍下MyBatis整合Spring的秘密

     

     

  • 相关阅读:
    第四章:控制结构
    字体族与图标字体
    如何构建高效、可观的系统「GitHub 热点速览」
    【牛客刷题-算法】NC151 最大公约数
    mysql安装
    (源码)电子采购管理系统建设方案和配套源码,srm管理
    『Java安全』Struts2 2.1.8.1 参数名OGNL注入漏洞S2-003复现与浅析
    基于马尔可夫随机场的图像去噪算法matlab仿真
    2022年最火的十大测试工具,你掌握了几个
    科技云报道:两会热议的数据要素,如何拥抱新技术?
  • 原文地址:https://blog.csdn.net/m0_67698950/article/details/125557839