Mybatis

171 阅读16分钟

Mybatis

1. Mybatis中#{}和${}的区别是什么?

#{}是预编译处理,${}是字符串替换。

  • mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值。

  • mybatis在处理时,就是把{}时,就是把{}替换成变量的值。

  • 使用#{}可以有效的防止SQL注入,提高系统安全性。原因在于:预编译机制。

预编译是提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译。我们知道,SQL注入是发生在编译的过程中,因为恶意注入了某些特殊字符,最后被编译成了恶意的执行操作。而预编译机制则可以很好的防止SQL注入。预编译完成之后,SQL的结构已经固定,即便用户输入非法参数,也不会对SQL的结构产生影响,从而避免了潜在的安全风险。

注:符号一般用来当作占位符,常使用Linux脚本。例如:符号一般用来当作占位符,常使用Linux脚本。例如:1,$2等等表示输入参数的占位符。

2. Mybatis有几种分页方式?

一. 借助数组进行分页

原理:进行数据库查询操作时,获取数据库中所有满足条件的记录,保存在应用的临时数组中,再通过List的subList方法,获取满足条件的所有记录。

实现:

首先在dao层,创建StudentMapper接口,用于对数据库的操作。在接口中定义通过数组分页的查询方法,如下所示:

	List<Student> queryStudentsByArray();

方法很简单,就是获取所有的数据,通过list接收后进行分页操作。

创建StudentMapper.xml文件,编写查询的sql语句:

   <select id="queryStudentsByArray"  resultMap="studentmapper">
          select * from student
   </select>

可以看出再编写sql语句的时候,我们并没有作任何分页的相关操作。这里是查询到所有的学生信息。

接下来在service层获取数据并且进行分页实现:

定义IStuService接口,并且定义分页方法:

  List<Student> queryStudentsByArray(int currPage, int pageSize);

通过接收currPage参数表示显示第几页的数据,pageSize表示每页显示的数据条数。

创建IStuService接口实现类StuServiceIml对方法进行实现,对获取到的数组通过currPage和pageSize进行分页:

      @Override
      public List<Student> queryStudentsByArray(int currPage, int pageSize) {
          List<Student> students = studentMapper.queryStudentsByArray();
  //        从第几条数据开始
          int firstIndex = (currPage - 1) * pageSize;
  //        到第几条数据结束
          int lastIndex = currPage * pageSize;
          return students.subList(firstIndex, lastIndex);
      }

通过subList方法,获取到两个索引间的所有数据。

最后在controller中创建测试方法:

	@ResponseBody
    @RequestMapping("/student/array/{currPage}/{pageSize}")
    public List<Student> getStudentByArray(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) {
        List<Student> student = StuServiceIml.queryStudentsByArray(currPage, pageSize);
        return student;
    }

缺点:

数据库查询并返回所有的数据,而我们需要的只是极少数符合要求的数据。当数据量少时,还可以接受。当数据库数据量过大时,每次查询对数据库和程序的性能都会产生极大的影响。

二. 借助Sql语句进行分页

在了解到通过数组分页的缺陷后,我们发现不能每次都对数据库中的所有数据都检索。然后在程序中对获取到的大量数据进行二次操作,这样对空间和性能都是极大的损耗。所以我们希望能直接在数据库语言中只检索符合条件的记录,不需要在通过程序对其作处理。这时,Sql语句分页技术横空出世。

实现:

通过sql语句实现分页也是非常简单的,只是需要改变我们查询的语句就能实现了,即在sql语句后面添加limit分页语句。

首先还是在StudentMapper接口中添加sql语句查询的方法,如下:

	List<Student> queryStudentsBySql(Map<String,Object> data);

然后在StudentMapper.xml文件中编写sql语句通过limiy关键字进行分页:

  <select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
          select * from student limit #{currIndex} , #{pageSize}
  </select>

接下来还是在IStuService接口中定义方法,并且在StuServiceIml中对sql分页实现。

   List<Student> queryStudentsBySql(int currPage, int pageSize);
 
    @Override
    public List<Student> queryStudentsBySql(int currPage, int pageSize) {
        Map<String, Object> data = new HashedMap();
        data.put("currIndex", (currPage-1)*pageSize);
        data.put("pageSize", pageSize);
        return studentMapper.queryStudentsBySql(data);
    }
  

缺点:

虽然这里实现了按需查找,每次检索得到的是指定的数据。但是每次在分页的时候都需要去编写limit语句,很冗余。而且不方便统一管理,维护性较差。所以我们希望能够有一种更方便的分页实现。

三. 拦截分页

上面提到的数组分页和sql语句分页都不是我们今天讲解的重点,今天需要实现的是利用拦截器达到分页的效果。自定义拦截器实现了拦截所有以ByPage结尾的查询语句,并且利用获取到的分页相关参数统一在sql语句后面加上limit分页的相关语句,一劳永逸。不再需要在每个语句中单独去配置分页相关的参数了。。。

首先我们看一下拦截器的具体实现,在这里我们需要拦截所有以ByPage结尾的所有查询语句,因此要使用该拦截器实现分页功能,那么再定义名称的时候需要满足它拦截的规则(以ByPage结尾),如下所示:

  /**
   * @Intercepts 说明是一个拦截器
   * @Signature 拦截器的签名
   * type 拦截的类型 四大对象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
   * method 拦截的方法
   * args 参数
   */
  @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
  public class MyPageInterceptor implements Interceptor {

  //每页显示的条目数
      private int pageSize;
  //当前现实的页数
      private int currPage;

      private String dbType;


      @Override
      public Object intercept(Invocation invocation) throws Throwable {
          //获取StatementHandler,默认是RoutingStatementHandler
          StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
          //获取statementHandler包装类
          MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);

          //分离代理对象链
          while (MetaObjectHandler.hasGetter("h")) {
              Object obj = MetaObjectHandler.getValue("h");
              MetaObjectHandler = SystemMetaObject.forObject(obj);
          }

          while (MetaObjectHandler.hasGetter("target")) {
              Object obj = MetaObjectHandler.getValue("target");
              MetaObjectHandler = SystemMetaObject.forObject(obj);
          }

          //获取连接对象
          //Connection connection = (Connection) invocation.getArgs()[0];


          //object.getValue("delegate");  获取StatementHandler的实现类

          //获取查询接口映射的相关信息
          MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
          String mapId = mappedStatement.getId();

          //statementHandler.getBoundSql().getParameterObject();

          //拦截以.ByPage结尾的请求,分页功能的统一实现
          if (mapId.matches(".+ByPage$")) {
              //获取进行数据库操作时管理参数的handler
              ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler");
              //获取请求时的参数
              Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject();
              //也可以这样获取
              //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();

              //参数名称和在service中设置到map中的名称一致
              currPage = (int) paraObject.get("currPage");
              pageSize = (int) paraObject.get("pageSize");

              String sql = (String) MetaObjectHandler.getValue("delegate.boundSql.sql");
              //也可以通过statementHandler直接获取
              //sql = statementHandler.getBoundSql().getSql();

              //构建分页功能的sql语句
              String limitSql;
              sql = sql.trim();
              limitSql = sql + " limit " + (currPage - 1) * pageSize + "," + pageSize;

              //将构建完成的分页sql语句赋值个体'delegate.boundSql.sql',偷天换日
              MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);
          }
  //调用原对象的方法,进入责任链的下一级
          return invocation.proceed();
      }


      //获取代理对象
      @Override
      public Object plugin(Object o) {
      //生成object对象的动态代理对象
          return Plugin.wrap(o, this);
      }

      //设置代理对象的参数
      @Override
      public void setProperties(Properties properties) {
  //如果项目中分页的pageSize是统一的,也可以在这里统一配置和获取,这样就不用每次请求都传递pageSize参数了。参数是在配置拦截器时配置的。
          String limit1 = properties.getProperty("limit", "10");
          this.pageSize = Integer.valueOf(limit1);
          this.dbType = properties.getProperty("dbType", "mysql");
      }
  }

原来它是通过不同的MappedStatement创建不同的StatementHandler实现类对象处理不同的情况。这里的到的StatementHandler实现类才是真正服务的。看到这里,你可能就会明白

MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement")

中delegate的来源了吧。至于为什么要这么去获取,后面我们会说道。

拿到statementHandler后,我们会通过

MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);

去获取它的包装对象,通过包装对象去获取各种服务。

MetaObject:mybatis的一个工具类,方便我们有效的读取或修改一些重要对象的属性。四大对象(ResultSetHandler,ParameterHandler,Executor和statementHandler)提供的公共方法很少,要想直接获取里面属性的值很困难,但是可以通过MetaObject利用一些技术(内部反射实现)很轻松的读取或修改里面的数据。

接下来说说:

MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");

上面提到为什么要这么去获取MappedStatement对象??在RoutingStatementHandler中delegate是私有的(private final StatementHandler delegate;),有没有共有的方法去获取。所以这里只有通过反射来获取啦。

MappedStatement是保存了xxMapper.xml中一个sql语句节点的所有信息的包装类,可以通过它获取到节点中的所有信息。在示例中我们拿到了id值,也就是方法的名称,通过名称区拦截所有需要分页的请求。

通过StatementHandler的包装类,不光能拿到MappedStatement,还可以拿到下面的数据:

    public abstract class BaseStatementHandler implements StatementHandler {
    protected final Configuration configuration;
    protected final ObjectFactory objectFactory;
    protected final TypeHandlerRegistry typeHandlerRegistry;
    protected final ResultSetHandler resultSetHandler;
    protected final ParameterHandler parameterHandler;
    protected final Executor executor;
    protected final MappedStatement mappedStatement;
    protected final RowBounds rowBounds;
    protected BoundSql boundSql;

    protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        this.configuration = mappedStatement.getConfiguration();
        this.executor = executor;
        this.mappedStatement = mappedStatement;
        this.rowBounds = rowBounds;
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
        this.objectFactory = this.configuration.getObjectFactory();
        if(boundSql == null) {
            this.generateKeys(parameterObject);
            boundSql = mappedStatement.getBoundSql(parameterObject);
        }

        this.boundSql = boundSql;
        this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
        this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql);
    }

上面的所有数据都可以通过反射拿到。

几个重要的参数:

  • Configuration:所有配置的相关信息。

  • ResultSetHandler:用于拦截执行结果的组装。

  • ParameterHandler:拦截执行Sql的参数的组装。

  • Executor:执行Sql的全过程,包括组装参数、组装结果和执行Sql的过程。

  • BoundSql:执行的Sql的相关信息。

接下来我们通过如下代码拿到请求时的map对象(反射)。

  //获取进行数据库操作时管理参数的handler
  ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler");
  //获取请求时的参数
  Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject();
  //也可以这样获取
  //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();

拿到我们需要的currPage和pageSize参数后,就是组装分页查询的sql语句’limitSql‘了。

最后通过MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);将原始的sql语句替换成我们新的分页语句,完成偷天换日的功能,接下来让代码继续执行。

编写好拦截器后,需要注册到项目中,才能发挥它的作用。在mybatis的配置文件中,添加如下代码:

    <plugins>
        <plugin interceptor="com.cbg.interceptor.MyPageInterceptor">
            <property name="limit" value="10"/>
            <property name="dbType" value="mysql"/>
        </plugin>
    </plugins>

到这里,有关拦截器的相关知识就讲解的差不多了,接下来就需要测试,是否我们这样写真的有效??

首先还是添加dao层的方法和xml文件的sql语句配置,注意项目中拦截的是以ByPage结尾的请求,所以在这里,我们的方法名称也以此结尾:

  //方法
  List<Student> queryStudentsByPage(Map<String,Object> data);

  //xml文件的select语句
  <select id="queryStudentsByPage" parameterType="map" resultMap="studentmapper">
      select * from student
  </select>

可以看出,这里我们就不需要再去手动配置分页语句了。

接下来是service层的接口编写和实现方法:

  //方法:
  List<Student> queryStudentsByPage(int currPage,int pageSize);

  //实现:
  @Override
  public List<Student> queryStudentsByPage(int currPage, int pageSize) {
      Map<String, Object> data = new HashedMap();
      data.put("currPage", currPage);
      data.put("pageSize", pageSize);
      return studentMapper.queryStudentsByPage(data);
  }

四. RowBounds实现分页

原理:通过RowBounds实现分页和通过数组方式分页原理差不多,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果。只是数组分页需要我们自己去实现分页逻辑,这里更加简化而已。

存在问题:一次性从数据库获取的数据可能会很多,对内存的消耗很大,可能导师性能变差,甚至引发内存溢出。

适用场景:在数据量很大的情况下,建议还是适用拦截器实现分页效果。RowBounds建议在数据量相对较小的情况下使用。

实现:

dao层接口方法:

  //加入RowBounds参数
  public List<UserBean> queryUsersByPage(String userName, RowBounds rowBounds);

然后在service层构建RowBounds,调用dao层方法:

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.SUPPORTS)
    public List<RoleBean> queryRolesByPage(String roleName, int start, int limit) {
        return roleDao.queryRolesByPage(roleName, new RowBounds(start, limit));
    }

RowBounds就是一个封装了offset和limit简单类,如下所示:

public class RowBounds {
    public static final int NO_ROW_OFFSET = 0;
    public static final int NO_ROW_LIMIT = 2147483647;
    public static final RowBounds DEFAULT = new RowBounds();
    private int offset;
    private int limit;

    public RowBounds() {
        this.offset = 0;
        this.limit = 2147483647;
    }

    public RowBounds(int offset, int limit) {
        this.offset = offset;
        this.limit = limit;
    }

    public int getOffset() {
        return this.offset;
    }

    public int getLimit() {
        return this.limit;
    }
}

RowBounds就是一个封装了offset和limit简单类,如下所示:

结论:从上面四种sql分页的实现方式可以看出,通过RowBounds实现是最简便的,但是通过拦截器的实现方式是最优的方案。只需一次编写,所有的分页方法共同使用,还可以避免多次配置时的出错机率,需要修改时也只需要修改这一个文件,一劳永逸。而且是我们自己实现的,便于我们去控制和增加一些逻辑处理,使我们在外层更简单的使用。同时也不会出现数组分页和RowBounds分页导致的性能问题。当然,具体情况可以采取不同的解决方案。数据量小时,RowBounds不失为一种好办法。但是数据量大时,实现拦截器就很有必要了。

3. RowBounds是一次性查询全部结果吗?为什么?

RowBounds 表面是在“所有”数据中检索数据,其实并非是一次性查询出所有数据,因为 MyBatis 是对 jdbc 的封装,在 jdbc 驱动中有一个 Fetch Size 的配置,它规定了每次最多从数据库查询多少条数据,假如你要查询更多数据,它会在你执行 next()的时候,去查询更多的数据。就好比你去自动取款机取 10000 元,但取款机每次最多能取 2500 元,所以你要取 4 次才能把钱取完。只是对于 jdbc 来说,当你调用 next()的时候会自动帮你完成查询工作。这样做的好处可以有效的防止内存溢出。

4. Mybatis逻辑分页和物理分页的区别是什么?

物理分页

物理分页依赖的是某一物理实体,这个物理实体就是数据库,比如MySQL数据库提供了limit关键字,程序员只需要编写带有limit关键字的SQL语句,数据库返回的就是分页结果。

逻辑分页(内存分页)

逻辑分页依赖的是程序员编写的代码。数据库返回的不是分页结果,而是全部数据,然后再由程序员通过代码获取分页数据,常用的操作是一次性从数据库中查询出全部数据并存储到List集合中,因为List集合有序,再根据索引获取指定范围的数据。

对比

逻辑分页物理分页
数据库负担逻辑分页只访问一次数据库物理分页每次都访问数据库,物理分页对数据库造成的负担大
服务器负担逻辑分页一次性将数据读取到内存,占用了较大的内容空间物理分页每次只读取一部分数据,占用内存空间较
实时性逻辑分页一次性将数据读取到内存,数据发生改变,数据库的最新状态不能实时反映到操作中,实时性差物理分页每次需要数据时都访问数据库,能够获取数据库的最新状态,实时性强。
适用场合逻辑分页主要用于数据量不大、数据稳定的场合物理分页主要用于数据量较大、更新频繁的场合。

5. Mybatis 是否支持延迟加载?延迟加载的原理是什么?

mybatis支持延迟加载

适用场景

  • 一对一,多对一 立即加载
  • 一对多,多对多 延迟加载

Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。

在 Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。它的原理是,

使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。

当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。

6. 说一下Mybatis的一级缓存和二级缓存?

缓存:将相同查询条件的sql语句执行一遍后所得到的结果存在内存或者某种缓存介质当中,当下次遇到一模一样的查询sql时候不在执行sql与数据库交互,而是直接从缓存中获取结果,减少服务器的压力;

mybatis的查询缓存, 又分为一级缓存和二级缓存,一级缓存的作用范围为同一个sqlsession,而二级缓存的作用范围为同一个namespace和mapper;

一级缓存

一级缓存是mybatis默认就帮我们开启的,我们不需要多做配置,但是我们得知道其中原理,否则我们也不知道怎么使用,也不知道我们到底有没有一级缓存。

上面第二部分说过一级缓存的作用域是同一个sqlsession,sqlsession的作用就是建立和数据库的会话,我们对数据库表的增删改查都是通过sqlsession去执行指定的sql完成的,而sqlsession和数据库的连接并不是永久连接的,也一定要杜绝这种永久连接;所以就有了sqlsession的创建和关闭,sqlsession默认执行完一段的sql片段后就会close掉sqlsession,即销毁sqlsession;而下一次对数据库的操作的又会重新建立会话关系,即建立新的sqlsession,所以这就和前一次的执行sql的sqlsession属于不同的SQL session了,也就这两个sqlsession就不存在一级缓存关系了;

二级缓存

二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,所以这里,我们需要对mybatis的配置修改,开启二级缓存设施,而且需要在我们的namespace下开启缓存,具体如下

在mybatis-config.xml文件中的标签配置开启缓存,代码如下:

  <!--开启缓存,此时配置的是mybatis的二级缓存-->
  <setting name="cacheEnabled" value="true"/>   

单单配置这个还是不够的,还需要在我们的mapper的xml文件下开启缓存,即加入该标签:

  <!--namespace必须加上此标签才会开启二级缓存-->
  <cache />

mybatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中,所以需要我们对pojo对象进行序列化,只要实现序列化接口即可,

7. Mybatis和Hibernate的区别有哪些?

  • Mybatis 和 hibernate 不同,它不完全是一个 ORM 框架,因为 MyBatis 需要程序员自己编写 Sql 语句。

  • Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文件,工作量大。

  • Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用 hibernate 开发可以节省很多代码,提高效率。

8. Mybatis有哪些执行器(Executor)?

Mybatis有三种基本的Executor执行器:

	SimpleExecutor、ReuseExecutor、BatchExecutor
  • SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。

  • BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

Mybatis中如何指定使用哪一种Executor执行器?

在Mybatis配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数。

9. Mybatis 分页插件的实现原理是什么?

MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。

MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理SQL,处理结果。

插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。

mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式,我们应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。只有清楚了这些对象的方法的作用,当我们自己编写插件的时候才知道从哪里去拦截。

Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。从代码顺序上能看到。我们可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

@Intercepts({//需要拦截的方法
   @Signature(type = Executor.class,method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(type = Executor.class,method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class MyPageInterceptor implements Interceptor {


    // 用于覆盖被拦截对象的原有方法(在调用代理对象Plugin 的invoke()方法时被调用)
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("将逻辑分页改为物理分页");
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
        BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
        RowBounds rb = (RowBounds) args[2]; // RowBounds
        // RowBounds为空,无需分页
        if (rb == RowBounds.DEFAULT) {
            return invocation.proceed();
        }// 在SQL后加上limit语句
        String sql = boundSql.getSql();
        String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
        sql = sql + " " + limit;

        // 自定义sqlSource
        SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings());

        // 修改原来的sqlSource
        Field field = MappedStatement.class.getDeclaredField("sqlSource");
        field.setAccessible(true);
        field.set(ms, sqlSource);

        // 执行被拦截方法
        return invocation.proceed();
    }

    // target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    // 设置参数
    @Override
    public void setProperties(Properties properties) {
    }
}

插件注册,在mybatis-config.xml 中注册插件:

  <plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
      <property name="offsetAsPageNum" value="true"/>
        ……后面全部省略……
    </plugin>
  </plugins>

代理和拦截的实现

上面提到的可以被代理的四大对象都是什么时候被代理的呢?Executor 是openSession() 的时候创建的; StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。代理是由Plugin 类创建。在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。

当个插件的情况下,代理能不能被代理?代理顺序和调用顺序的关系? 可以被代理。

因为代理类是Plugin,所以最后调用的是Plugin 的invoke()方法。它先调用了定义的拦截器的intercept()方法。可以通过invocation.proceed()调用到被代理对象被拦截的方法。  调用流程时序图:

10.Mybatis如何编写一个自定义插件?

1. 编写Interceptor的实现类
2. 使用@Intercepts注解完成插件签名 说明插件的拦截四大对象之一的哪一个对象的哪一个方法
3. 将写好的插件注册到全局配置文件中