Mybatis源码(四)插件实现及原理

mybatis插件,实际上是个拦截器,类似spring中的拦截器。思想就是在执行相关sql操作时,拦截并修改要执行的sql。比如分页插件,就是拦截到sql然后在sql中添加分页参数。本文就是通过实现简单的分页插件,来分析插件的编写以及执行过程。

因为编写插件的流程套路都是一样的,下面直接上demo,结合demo分析原理。

1.demo

这里自定义一个插件类,也就是拦截器的实现类:

@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class MyMybatisInterceptor implements Interceptor {
    private Integer defaultPageIndex = 1;
    private Integer defaultPageSize = 10;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //取出被拦截对象
        StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(stmtHandler);
       // .....省略后续代码

这里先贴一部分代码,因为有几个点需要先说一下。

1.1 核心接口 Interceptor

上述自定义的插件,实现了接口Interceptor。这个接口是mybatis给出的,就是用于编写插件。自定义插件,就是实现这个接口的3个方法,下面看下接口方法:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);
}
  • intercept 方法,核心方法,实现这个方法,用来覆盖被拦截对象的原有方法。其参数Invocation对象,用于反射调用原来对象方法。此方法在执行mapper方法时被执行。
  • plugin 方法,其入参target是被拦截的对象,此方法实现用来生成目标对象的代理对象。此方法在执行mapper方法时被执行。
  • setProperties 方法,用来设置参数,比如pageSize等初始化参数的值。参数值在xml中配置,这个方法在mabatis初始化的时候,会被调用。

1.2 @Intercepts注解

上面demo除了实现了Interceptor接口,还要添加@Intercepts注解,并且写明一些参数内容,比如@Signature等。@Intercepts表示此类是个拦截器,不写此注解,执行会抛异常。
其参数值这里说一下:
@Signature表示签名,就是表明,此拦截器具体拦截哪些对象的哪些内容。

@Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
  • type 表示拦截的对象类型,是四大对象中的一个,具体看下文描述。
  • method 表示要拦截的对象的某一种接口方法。
  • args 表示要拦截方法的的参数。

a. 拦截的 四大对象类型

  • Executor 是执行SQL的全过程,包括组装参数,组装结果集,执行sql过程。一般不会拦击此对象。
  • StatementHandler 是执行sql的过程,是最常用的拦截对象,本文demo就是拦截的此对象,可以用来重写执行sql的过程。
  • ParameterHandler 主要拦截用于sql执行的参数,可以拦截后重新组装参数。
  • ResultSetHandler 用于拦截执行后的结果,我们可以拦截然后重新组装结果。

b. 拦截的方法
上面四种拦截对象确认后,比如我们需要拦截StatementHandler对象,那么接下来,就要指定具体拦截此对象的哪个方法。
在整个mapper执行过程中,是通过Executor调度StatementHandler来完成,比如调度StatementHandler的prepare方法来完成sql的预编译,于是我们需要拦截此prepare方法来修改sql。

StatementHandler对象有如下接口方法:

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)throws SQLException;

  void parameterize(Statement statement) throws SQLException;

  void batch(Statement statement) throws SQLException;

  int update(Statement statement) throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement) throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();
}

以上方法都可以被拦截。

c. 拦截的参数
确定了拦截的方法之后,接下来需要指定拦截方法的参数。prepare方法有两个参数:
prepare(Connection connection, Integer transactionTimeout)

因此最终拦截器签名如下:

@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})

1.3 完整demo

虽然网上demo比较多,但有些还是不能执行的。这里还是贴一下:

MyMybatisInterceptor.java

@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class MyMybatisInterceptor implements Interceptor {
    private Integer defaultPageIndex = 1;
    private Integer defaultPageSize = 10;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //取出被拦截对象StatementHandler
        //MetaObject是mybatis提供的工具类,利用反射获取对象中的各种内容
        StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(stmtHandler);

        // 分离代理对象,由于可能存在多个插件,从而形成多次代理,这里通过两次循环得到最原始的被代理类,MyBatis使用的是JDK代理
        while (metaObject.hasGetter("h")) {
            Object object = metaObject.getValue("h");
            metaObject = SystemMetaObject.forObject(object);
        }
        // 分离最后一个代理对象的目标类
        while (metaObject.hasGetter("target")) {
            Object object = metaObject.getValue("target");
            metaObject = SystemMetaObject.forObject(object);
        }

        //检查是否是select查询,因为是分页插件,不是select的不用处理
        String sql = getSql(metaObject);
        if (!checkSelect(sql)) {
            // 不是select语句,进入责任链下一层
            return invocation.proceed();
        }

        //得到查询的绑定对象
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        //得到mapper接口参数
        Object parameterObject = boundSql.getParameterObject();
        PageParams pageParams = getPageParamsForParamObj(parameterObject);
        if (pageParams == null) {
            // 没有传入page对象,不执行分页处理,进入责任链下一层
            return invocation.proceed();
        }

        // 设置分页默认值
        if (pageParams.getPageIndex() == null) {
            pageParams.setPageIndex(this.defaultPageIndex);
        }
        if (pageParams.getPageSize() == null) {
            pageParams.setPageSize(this.defaultPageSize);
        }

        // 设置分页总数,数据总数
        setTotalToPage(pageParams, invocation, metaObject, boundSql);

        // 校验分页参数
        checkPage(pageParams);

        return changeSql(invocation, metaObject, boundSql, pageParams);
    }

    @Override
    public Object plugin(Object target) {
        // 生成代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 初始化配置的默认页码,无配置则默认1
        this.defaultPageIndex = Integer.parseInt(properties.getProperty("default.pageIndex", "1"));
        // 初始化配置的默认数据条数,无配置则默认20
        this.defaultPageSize = Integer.parseInt(properties.getProperty("default.pageSize", "20"));

    }
    /**
     * 判断是否是select语句
     *
     * @param sql
     * @return
     */
    private boolean checkSelect(String sql) {
        // 去除sql的前后空格,并将sql转换成小写
        sql = sql.trim().toLowerCase();
        return sql.indexOf("select") == 0;
    }
    /**
     * 获取分页参数
     *
     * @param parameterObject
     * @return
     */
    private PageParams getPageParamsForParamObj(Object parameterObject) throws Exception {
        PageParams pageParams = null;
        if (parameterObject == null) {
            return null;
        }

        if (parameterObject instanceof Map) {
            // 如果传入的参数是map类型的,则遍历map取出Page对象
            @SuppressWarnings("unchecked")
            Map<String, Object> paramMap = (Map<String, Object>) parameterObject;
            Set<String> keySet = paramMap.keySet();
            for (String key : keySet) {
                Object value = paramMap.get(key);
                if (value instanceof PageParams) {
                    // 返回Page对象
                    return (PageParams) value;
                }
            }
        } else if (parameterObject instanceof com.example.demo.model.PageParams) {
            // 如果传入的是Page类型,则直接返回该对象
            return (PageParams) parameterObject;
        } else {
            Field[] fields = parameterObject.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.getType() == PageParams.class) {
                    PropertyDescriptor pd = new PropertyDescriptor(field.getName(), parameterObject.getClass());
                    Method method = pd.getReadMethod();
                    return (PageParams) method.invoke(parameterObject);
                }
            }
        }

        // 初步判断并没有传入Page类型的参数,返回null
        return pageParams;
    }

    /**
     * 获取数据总数
     *
     * @param invocation
     * @param metaObject
     * @param boundSql
     * @return
     */
    private int getTotal(Invocation invocation, MetaObject metaObject, BoundSql boundSql) {
        // 获取当前的mappedStatement对象
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // 获取配置对象
        Configuration configuration = mappedStatement.getConfiguration();
        // 获取当前需要执行的sql
        String sql = getSql(metaObject);
        // 改写sql语句,实现返回数据总数 $_paging取名是为了防止数据库表重名
        String countSql = "select count(*) as total from (" + sql + ") $_paging";
        // 获取拦截方法参数,拦截的是connection对象
        Connection connection = (Connection) invocation.getArgs()[0];
        PreparedStatement pstmt = null;
        int total = 0;

        try {
            // 预编译查询数据总数的sql语句
            pstmt = connection.prepareStatement(countSql);
            // 构建boundSql对象
            BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(),
                    boundSql.getParameterObject());
            // 构建parameterHandler用于设置sql参数
            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(),
                    countBoundSql);
            // 设置sql参数
            parameterHandler.setParameters(pstmt);
            //执行查询
            ResultSet rs = pstmt.executeQuery();
            while (rs.next()) {
                total = rs.getInt("total");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (pstmt != null) {
                try {
                    pstmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

        // 返回总数据数
        return total;
    }

    /**
     * 设置总数据数、总页数
     *
     * @param page
     * @param invocation
     * @param metaObject
     * @param boundSql
     */
    private void setTotalToPage(PageParams page, Invocation invocation, MetaObject metaObject, BoundSql boundSql) {
        // 总数据数
        int total = getTotal(invocation, metaObject, boundSql);
        // 计算总页数
        int totalPage = total / page.getPageSize();
        if (total % page.getPageSize() != 0) {
            totalPage = totalPage + 1;
        }

        page.setTotal(total);
        page.setTotalPage(totalPage);
    }

    /**
     * 校验分页参数
     *
     * @param page
     */
    private void checkPage(PageParams page) {
        // 如果当前页码大于总页数,抛出异常
        if (page.getPageIndex() > page.getTotalPage()) {
            throw new RuntimeException("当前页码[" + page.getPageIndex() + "]大于总页数[" + page.getTotalPage() + "]");
        }
        // 如果当前页码小于总页数,抛出异常
        if (page.getPageIndex() < 1) {
            throw new RuntimeException("当前页码[" + page.getPageIndex() + "]小于[1]");
        }
    }

    /**
     * 修改当前查询的sql
     *
     * @param invocation
     * @param metaObject
     * @param boundSql
     * @param page
     * @return
     */
    private Object changeSql(Invocation invocation, MetaObject metaObject, BoundSql boundSql, PageParams page) throws Exception {
        // 获取当前查询的sql
        String sql = getSql(metaObject);
        // 修改sql,$_paging_table_limit取名是为了防止数据库表重名
        String newSql = "select * from (" + sql + ") $_paging_table_limit limit ?, ?";
        // 设置当前sql为修改后的sql
        setSql(metaObject, newSql);

        // 获取PreparedStatement对象
        PreparedStatement pstmt = (PreparedStatement) invocation.proceed();
        // 获取sql的总参数个数
        int parameCount = pstmt.getParameterMetaData().getParameterCount();
        // 设置分页参数
        pstmt.setInt(parameCount - 1, (page.getPageIndex() - 1) * page.getPageSize());
        pstmt.setInt(parameCount, page.getPageSize());

        return pstmt;
    }

    /**
     * 获取当前查询的sql
     *
     * @param metaObject
     * @return
     */
    private String getSql(MetaObject metaObject) {
        return (String) metaObject.getValue("delegate.boundSql.sql");
    }

    /**
     * 设置当前查询的sql
     *
     * @param metaObject
     */
    private void setSql(MetaObject metaObject, String sql) {
        metaObject.setValue("delegate.boundSql.sql", sql);
    }
}

PageParams.java

@Data
public class PageParams {
	
    private Integer pageIndex;

    private Integer pageSize;

    private Integer total;

    private Integer totalPage;
}
@Mapper
public interface UsersMapper {
    List<Users> selectAll(PageParams pageParams);
}

配置信息

@Configuration
public class MybatisConfig {
    @Bean
    public MyMybatisInterceptor myMybatisInterceptor() {
        return new MyMybatisInterceptor();
    }
}

如果使用mybatis 的xml配置文件,也可以在mybatis-config.xml配置文件中配置自定义的分页插件:

<plugins>
    <plugin interceptor="xx.xxx.xxxx.MyMybatisInterceptor">
        <property name="default.pageIndex" value="1"/>
        <property name="default.pageSize" value="10"/>
    </plugin>
</plugins>

2 插件执行流程

下面分析下,插件是何时如何被加载,然后何时如何被执行的。本文是基于springboot的分析的,因为目前大部分开发工作都是基于springboot。

2.1 加载过程

插件的加载,是在springboot的启动过程中完成的。springboot启动,会初始化maybatis,这个在前面的两篇文章中有分析。下面调重点捋一下流程:

mybatis在springboot中的初始化配置类MybatisAutoConfiguration

public class MybatisAutoConfiguration {
  private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
  private final MybatisProperties properties;

//拦截器数组,用于保存初始化加载的所有拦截器
  private final Interceptor[] interceptors;

  private final ResourceLoader resourceLoader;
  private final DatabaseIdProvider databaseIdProvider;
  private final List<ConfigurationCustomizer> configurationCustomizers;

  public MybatisAutoConfiguration(MybatisProperties properties,
                                  ObjectProvider<Interceptor[]> interceptorsProvider,
                                  ResourceLoader resourceLoader,
                                  ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                  ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
    this.properties = properties;
    
    //这里获取到了所有拦截器
    this.interceptors = interceptorsProvider.getIfAvailable();
    
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
  }
  //后续代码省略。。。。

上述配置类的构造方法中,有如下内容:
this.interceptors = interceptorsProvider.getIfAvailable();参数interceptorsProvider是从参数中传入:
ObjectProvider<Interceptor[]> interceptorsProvider,

这里ObjectProvider是比较关键的,不熟悉的话可以查阅其他资料,简单来说ObjectProvider是spring提供,用来注入bean对象的,其泛型参数是Interceptor[]拦截器数组,也就是用来注入拦截器数组的。执行完interceptorsProvider.getIfAvailable()这个方法,所有实现了Interceptor的拦截器(插件)都会被获取到。

然后,同样在MybatisAutoConfiguration中,在初始化SqlSessionFactory对象的时候,有如下内容:

@Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
   //省略若干代码
   //。。。。
   
    if (this.properties.getConfigurationProperties() != null) {
      factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }
    //这里如果拦截器不是空,则set到SqlSessionFactoryBean中
    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }
    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }
//省略若干代码
   //。。。。

最终启动代码执行到 SqlSessionFactoryBean#buildSqlSessionFactory() 方法中的时候,有如下内容:

  if (!isEmpty(this.plugins)) {
    for (Interceptor plugin : this.plugins) {
      configuration.addInterceptor(plugin);
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Registered plugin: '" + plugin + "'");
      }
    }
  }

上述代码,把所有的插件plugin,都add到了Configuration配置对象中保存,打开Configuration#addInterceptor方法:

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

interceptorChain是拦截器链对象,这里也说明,mybatis拦截器执行,使用的是责任链模式。
到这里,所有插件,都被加载保存到了Configuration对象中,这个对象相当于spring的应用上下文,后边sql执行的时候,所有内容都从这个对象中获取。

2.2 执行过程

上面,mybatis已经得到了所有拦截器,并保存到全局应用上下文中。
然后在我们调用mapper的方法时,就会执行。

切入点 plugin 方法

上面说到插件要实现接口Interceptor接口,其中有个plugin方法,我们有如下实现:

@Override
    public Object plugin(Object target) {
        // 生成代理对象
        return Plugin.wrap(target, this);
    }

我们就从这个方法切入,分析插件执行前后过程,下面先分析进入这个方法之后的事情,然后在分析如何进入这个方法的。

plugin方法作用

public class Plugin implements InvocationHandler {
.....
}

可以看到此类实现jdk的InvocationHandler,属性jdk动态代理的基本上就已经明白此类的作用了。
这个方法的作用就是生成代理类,代理原有执行对象,本文demo是StatementHandler对象。插件执行的时候,就是先执行了这个plugin方法,然后执行intercept实现方法。

其中Plugin类是mybatis提供给开发者使用的插件类,我们可以直接使用Plugin#wrap实现代理,这样我们就不用自己写那一大坨动态代理的实现代码了,看下Plugin#wrap的具体内容:

 public static Object wrap(Object target, Interceptor interceptor) {
 	//interceptor就是我们自定义的插件对象,这里可以获取到其上的具体的签名信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    
    //target是被代理的原始对象,本demo是StatementHandler对象(实现类是RoutingStatementHandler)
    Class<?> type = target.getClass();
    
    //从签名信息中得到接接口信息,本demo是StatementHandler
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    
    //这个if是判断参数target是不是是命中指定的type,是的话就对其代理,否则不处理直接return 
    if (interfaces.length > 0) {
    //jdk动态代理
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
  • 此方法生成代理类,代理的就是我们在签名中指定的type对象;
  • mybatis使用的jdk动态代理生成代理类。

如何进入plugin方法
上面我们已经知道,插件是使用jdk动态代理,代理的是StatementHandler对象的prepare方法,那么当执行prepare方法时,代码就会进入我们的intercept方法中。

StatementHandler#prepare最终是在如下方法中执行(前序方法可以在此打断点查看):
SimpleExecutor#prepareStatement

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    //这里执行prepare方法,如果有代理,则进入代理类
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

到此,就进入了我们自定义插件(拦截器)中。

发布了62 篇原创文章 · 获赞 29 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/csdn_20150804/article/details/102768718