mybatis

117 阅读10分钟

1.Mybatis-config.xml 文件的每项设置

1.1 configuration(配置)顶级标签

在 configuration 标签内部可以进行mybatis全局属性的配置,所有的配置都要在configuration 内部进行

1.2 属性(properties)

可以进入外部的 properties 文件,比如 数据库连接文件

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

或者

<properties resource="org/mybatis/example/config.properties"/>

设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值。比如:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

1.3 设置(settings)

<settings>
  <!--开启缓存:默认开启-->
  <setting name="cacheEnabled" value="true"/>
  <!--开启延迟加载,当开启时,所有关联对象都会延迟加载:默认关闭-->
  <setting name="lazyLoadingEnabled" value="true"/>
 
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <!--是否开启驼峰命名自动映射:默认关闭-->
  <setting name="mapUnderscoreToCamelCase" value="true"/>
  <!--默认值为 SESSION,会缓存一个会话中执行的所有查询。-->
  <setting name="localCacheScope" value="SESSION"/>
</settings>

1.4 类型别名(typeAliases)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,降低冗余的全限定类名书写。例如:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

当这样配置时,Blog 可以用在任何使用 domain.blog.Blog 的地方。

也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

1.4 类型处理器(typeHandlers)

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。

你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型.

<!-- mybatis-config.xml -->
<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

1.5 插件(plugins)

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)
// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}
<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。

1.6 环境配置(environments)

MyBatis 可以配置成适应多种环境 environments 元素定义了如何配置环境。

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

注意一些关键点:

  • 默认使用的环境 ID(比如:default="development")。
  • 每个 environment 元素定义的环境 ID(比如:id="development")。
  • 事务管理器的配置(比如:type="JDBC")。
  • 数据源的配置(比如:type="POOLED")。

默认环境和环境 ID 顾名思义。 环境可以随意命名,但务必保证默认的环境 ID 要匹配其中一个环境 ID。

事务管理器(transactionManager)

在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):

  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。

  • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。例如:

    <transactionManager type="MANAGED">
      <property name="closeConnection" value="false"/>
    </transactionManager>
    

数据源(dataSource)

dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。

  • 大多数 MyBatis 应用程序会按示例中的例子来配置数据源。虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。

有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):

  • UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接,不使用数据池连接
  • POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间
  • JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。

1.7 映射器(mappers)

用来设置 xxxMapper.xml 文件的位置

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

或者

<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

2.收货地址Address的Mapper接口与xml文件(增删改查)

addressMapper.java

package com.xhxy.eshop.mapper;

import java.util.List;

import com.xhxy.eshop.entity.Address;

/**
 * @author 牛珂凡
 */
public interface AddressMapper {

   /**
    * 查询某个用户的所有收货地址
    * @param userId 用户 id
    * @return 该用户所有收货地址
    */
   public List<Address> findByUserId(Integer userId);

   /**
    * 查询某个收货地址
    * @param id 地址 id
    * @return 某个收货地址
    */
   public Address findById(Integer id);

   /**
    * 为某个用户增加收货地址
    * @param address 地址
    * @return 返回 影响条数
    */
   public Integer add(Address address);

   /**
    * 更新某个收货地址
    * @param address 地址
    * @return 返回 影响条数
    */
   public Integer update(Address address);

   /**
    * 删除某个收货地址
    * @param id 要删除的地址id
    * @return 影响条数
    */
   public Integer delete(Integer id);
}

AddressMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
   "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 根元素是mapper,该元素的namespace属性值相当于该mapper的唯一标识 -->    
<mapper namespace="com.xhxy.eshop.mapper.AddressMapper">

    <!--
      id:接口的方法名
      resultType:返回的类型
    -->
   <select id="findByUserId" resultType="com.xhxy.eshop.entity.Address">
      select * from address where user_id = #{userId}    
   </select>
   <select id="findById" resultType="com.xhxy.eshop.entity.Address">
      select * from address where id = #{id}
   </select>
   
   <insert id="add">
      insert into address(consigneeName,consigneeAddress,consigneePhone,postcode,user_id)
      values(#{consigneeName},#{consigneeAddress},#{consigneePhone},#{postcode},#{user.id})
   </insert>
   
   <update id="update">
      update address set consigneeName=#{consigneeName},
         consigneeAddress=#{consigneeAddress},consigneePhone=#{consigneePhone},postcode=#{postcode}
          where id = #{id}
   </update>
   
   <delete id="delete">
      delete from address where id = #{id}
   </delete>
   
</mapper>

3.分类Category的自联系、1对多联系

CategoryMapper.java

/**
 * @author 牛珂凡
 */
public interface CategoryMapper {
   /**
    * 获取某个id号的分类
    * @param id
    * @return
    */
   public Category findById(Integer id);

   /**
    * 获取全部的分类项
    * @return
    */
   public List<Category> findAll();

   /**
    * 获取顶层分类项
    * @return
    */
   public List<Category> findTopCategory();

   /**
    * 获取某顶层分类项的所有子层分类
    * @param parentId
    * @return
    */
   public List<Category> findChildCategory(Integer parentId);
}

CategoryMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
   "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 根元素是mapper,该元素的namespace属性值相当于该mapper的唯一标识 -->    
<mapper namespace="com.xhxy.eshop.mapper.CategoryMapper">
   
   <select id="findById" resultMap="categoryMap">
      select * from category where id = #{id}       
   </select>
   
   <select id="findAll" resultMap="categoryMap">
      select * from category    
   </select>
   
   <select id="findTopCategory" resultMap="categoryMap">
      select * from category where grade = 0    
   </select>
   
   <resultMap type="com.xhxy.eshop.entity.Category" id="categoryMap">
      <id column="id" property="id"/>
      <result column="name" property="name"/>
      <result column="grade" property="grade"/>
      <result column="icon" property="icon"/>
      <collection column="id" property="children" 
         javaType="ArrayList"
         ofType="com.xhxy.eshop.entity.Category"
         select="com.xhxy.eshop.mapper.CategoryMapper.findChildCategory"
         fetchType="lazy"/>
   </resultMap>
   
   <select id="findChildCategory" resultType="com.xhxy.eshop.entity.Category">
      select * from category where parent = #{parentId}
   </select>  
   
</mapper>

商品和分类之间是1对多的关系,一个商品只能存在于一个分类中,一个分类可以有很多商品

分类和分类之间存在自联系,一个父分类可以就很多子分类,但是一个子分类只能有一个父分类

  • 获取某个id的分类(商品详情页)
  • 获取全部的分类项
    1.先查询的是顶级分类
    2.这里返回的结果必须是下面定义的resultMap
    <select id="findAll" resultMap="categoryMap"> 
        select * from category 
    </select>
    
     1.collection中的column属性可以为多个值,这里只有一个值,作为递归查询传递进去的参数
     2.ofType和javaType正好联合构成Category类中的children属性的数据类型
     3.select是递归查询用到的id
     <resultMap type="com.xhxy.eshop.entity.Category" id="categoryMap">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="grade" property="grade"/>
        <result column="icon" property="icon"/>
        <collection column="id" property="children" 
           javaType="ArrayList"
           ofType="com.xhxy.eshop.entity.Category"
           select="com.xhxy.eshop.mapper.CategoryMapper.findChildCategory"
           fetchType="lazy"/>
     </resultMap>
     
     1.利用上面查询结果collection中的column传入的id值进行递归查询,查出所有二级分类
     <select id="findChildCategory" resultType="com.xhxy.eshop.entity.Category">
        select * from category where parent = #{parentId}
     </select>  
    

4.多表连接、多对多联系

EvaluationMapper

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
   "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 根元素是mapper,该元素的namespace属性值相当于该mapper的唯一标识 -->    
<mapper namespace="com.xhxy.eshop.mapper.EvaluationMapper">
   
   1.根据商品id查询评价列表
   2.根据下面定义的resultMap进行查询
   3.通过association传入查询出来的user_id查询出评价的用户
   4.通过association传入查询出来的product_id查询出评价的商品
   <select id="findByProductId" resultMap="evaluationMap">
      select * from evaluation where product_id = #{productId}
   </select>
   <resultMap type="com.xhxy.eshop.entity.Evaluation" id="evaluationMap">
      <id column="id" property="id"/>
      <result column="content" property="content"/>
      <result column="createtime" property="createTime"/>
      <association property="user" column="user_id"
         javaType="com.xhxy.eshop.entity.User" 
         select="com.xhxy.eshop.mapper.UserMapper.findById"
         fetchType="lazy"/>
      <association property="product" column="product_id"
         javaType="com.xhxy.eshop.entity.Product" 
         select="com.xhxy.eshop.mapper.ProductMapper.findById"
         fetchType="lazy"/>
   </resultMap>
</mapper>

5. Cart到order生成的批量插入

  • 一个用户对应一个购物车
  • 一个购物车可以有多个购物车项
  • 每一个购物车项可以有多个商品

用户添加商品到购物车

<insert id="add">
   insert into 
   cartitem(product_id,quantity,total,cart_id) 
   values(#{product.id},#{quantity},#{total},#{cartId})   
</insert>

用户生成订单:购买多个商品,生成订单项,一类商品对应一个订单项,因此会有多个订单项生成,需要添加到数据库

<insert id="batchSave" parameterType="java.util.List" useGeneratedKeys="false">
   insert into orderitem(product_id,quantity,order_id,total) values
   <foreach collection="list" item="orderItem" index="index" separator=",">
       (
      #{orderItem.product.id}, #{orderItem.quantity},
      #{orderItem.order.id},#{orderItem.total}
        )
     </foreach >
</insert>

7.动态SQL的个人信息编辑

修改用户的个人信息,通过动态SQL来实现

通过用户id来确定一个用户,来进行修改

如果密码不为空,再将密码拼接到SQL中

<update id="update">
   update user 
   <!-- set元素可以去除多余的逗号 -->
   <set>
      username=#{username}, 
      <!-- 当password不为null,增加对password的更新 -->
      <if test="password!=null">password=#{password}, </if>
      phone=#{phone},email=#{email}, 
      <!-- 当avatar不为null,增加对avatar的更新 -->
      <if test="avatar != null">avatar=#{avatar} </if>
   </set> 
    where id = #{id}
</update>

8.继承映射的实现

添加了blog的发布者和所属部门,发布文章是属于管理员的功能,需要添加一个Admin类,添所属部门

public class Admin extends User {
   
   private String department;

   public String getDepartment() {
      return department;
   }

   public void setDepartment(String department) {
      this.department = department;
   }
   
}

image.png 通过usertype=1来确定他是一个管理员

当查询一个用户的时候,他可能是普通用户也可能是管理员,需要通过resuleMap来映射

<select id="findById" resultMap="userMap">
   select * from user where id = #{id}
</select>


<resultMap type="com.xhxy.eshop.entity.User" id="userMap">
   <id column="id" property="id"/>
   <result column="username" property="username"/>
   <result column="password" property="password"/>
   <result column="email" property="email"/>
   <result column="phone" property="phone"/>
   <result column="avatar" property="avatar"/>
   <!--使用结果值来决定使用哪个 `resultMap`-->
   <discriminator javaType="int" column="user_type">
      <!-- 当辨别者字段为1时,代表该记录对应admin实例,使用下面的adminMap映射 -->
      <case value="1" resultMap="adminMap">
      </case>
   </discriminator>
</resultMap>

 <!--adminmap继承自上面的userMap,拥有userMap的所有属性,在此基础上添加了一个部门属性-->
<resultMap type="com.xhxy.eshop.entity.Admin" id="adminMap" extends="userMap">
   <result column="department" property="department"/>
</resultMap>

在查询文章blog的时候要显示管理员字段

<select id="findById" resultMap="blogMap">
   select * from blog where id = #{id}
</select>
<resultMap type="com.xhxy.eshop.entity.Blog" id="blogMap">
   <id column="id" property="id"/>
   <result column="title" property="title"/>
   <result column="content" property="content"/>
   <result column="pic" property="pic"/>
   <result column="createtime" property="createTime"/>
   <!-- 注意:association 要在 collection的上面 -->
   <!-- 提交的管理员 -->
   <association property="admin" column="admin_id"
      javaType="com.xhxy.eshop.entity.Admin" 
      select="com.xhxy.eshop.mapper.UserMapper.findById"
      fetchType="lazy"/>
   <!-- 对该blog的所有评论 -->
   <collection column="id" property="commentList" 
      javaType="ArrayList"
      ofType="com.xhxy.eshop.entity.Comment" 
      select="com.xhxy.eshop.mapper.CommentMapper.findByBlogId"
      fetchType="lazy"/>
</resultMap>

9.缓存

mybatis的缓存分为一级缓存和二级缓存

一级缓存是基于 mybatis的 本地缓存,作用范围为 session 域内。当 session 关闭之后,该 session 中所有的 cache(缓存)就会被清空。

在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用同一个 mapper 的方法,往往只执行一次 SQL。因为使用 SqlSession 第一次查询后,MyBatis 会将其放在缓存中,再次查询时,如果没有刷新,并且缓存没有超时的情况下,SqlSession 会取出当前缓存的数据,而不会再次发送 SQL 到数据库。\

二级缓存是全局缓存,作用域超出 session 范围之外,可以被所有 SqlSession 共享。

一级缓存缓存的是 SQL 语句,二级缓存缓存的是结果对象。

1)MyBatis 的全局缓存配置需要在 mybatis-config.xml 的 settings 元素中设置,代码如下。

<settings>    
    <setting name="cacheEnabled" value="true" />
</settings>

2)在 mapper 文件(如 BlogMapper.xml)中设置缓存,默认不开启缓存。需要注意的是,二级缓存的作用域是针对 mapper 的 namescape 而言,即只有再次在 namescape 内的查询才能共享这个缓存

二级缓存失效的情况:
两次查询之间执行了任意增删改操作,会使一级和二级缓存同时失效

<!-- 定义缓存,使用FIFO清除算法,缓存刷新间隔为600秒,最多缓存64项,只读缓存 -->
<cache eviction="FIFO" flushInterval="600000" size="64" readOnly="true" />

10.拦截器与分页插件

page

package com.xhxy.eshop.interceptor;


import java.util.List;

/**
 * @author 牛珂凡
 */
public class Page<T> {
   private Integer totalPage;    // 总页数
   private Integer pageSize = 3;  // 每页的条数
   private Integer pageIndex = 1; // 当前页号

   private List<T> data;

   public void setData(List<T> data) {
      this.data = data;
   }

   public List<T> getData() {
      return data;
   }

   @Override
   public String toString() {
      return "Page{" +
            "totalPage=" + totalPage +
            ", pageSize=" + pageSize +
            ", pageIndex=" + pageIndex +
            '}';
   }

   // 初始化全部成员变量的构造器
   public Page(Integer pageSize, Integer pageIndex)
   {
      this.pageSize = pageSize;
      this.pageIndex = pageIndex;
   }

   // totalPage的setter和getter方法
   public void setTotalPage(Integer totalPage)
   {
      this.totalPage = totalPage;
   }
   public Integer getTotalPage()
   {
      return this.totalPage;
   }

   // pageSize的getter方法
   public Integer getPageSize()
   {
      return this.pageSize;
   }

   // pageIndex的getter方法
   public Integer getPageIndex()
   {
      return this.pageIndex;
   }
}

插件拦截器


@Intercepts({
      // 指定该插件拦截StatementHandler的prepare(Connection, Integer)方法
      @Signature(type = StatementHandler.class, method = "prepare",
            args = {Connection.class, Integer.class }) })
public class PagePlugin implements Interceptor
{
   @Override
   @SuppressWarnings("unchecked")
   public Object intercept(Invocation invocation) throws Throwable
   {
      // 1.获取被拦截的目标对象
      StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
      // 2.获取statementHandler对应的MetaObject对象 元数据
      MetaObject metaObject = SystemMetaObject.forObject(statementHandler);  // ①
      // 3.通过MetaObject获取本次执行的MappedStatement对象
      // MappedStatement代表XML Mapping中的select, insert、update, delete元素
      MappedStatement mappedStatement = (MappedStatement) metaObject
            .getValue("delegate.mappedStatement");  // ②
      // 4.获取执行的MappedStatement的id属性值(对应于Mapper组件的方法名)
      String id = mappedStatement.getId();
      // 5.如果方法名以Page结尾,说明是需要分页的方法
      if (id.endsWith("Page")) {
         //6.得到具体的SQL
         BoundSql boundSql = statementHandler.getBoundSql();
         //7.获取传给Mapper方法的参数
         Map<String,Object> paramMap = (Map<String, Object>) boundSql
               .getParameterObject();
         //8.定义page变量用于保存分页参数
         Page page = null;
         //9.先尝试获取名为page的命名参数(以@Param("page")修饰的参数)
         try {
            page = (Page) paramMap.get("page");
         }
         // 如果没找到名为page的命名参数
         catch (BindingException ex)
         {
            // 遍历paramMap(paramMap代表传给Mapper方法的实际参数)
            for (String key : paramMap.keySet())
            {
               Object val = paramMap.get(key);
               // 如果该参数的类型是Page,说明找到了page参数
               if (val.getClass() == Page.class) {
                  page = (Page) val;
               }
            }
         }
         // 如果page依然为null,说明没法找到分页参数
         if (page == null)
         {
            throw new IllegalArgumentException("Page Parameter can't be null.");
         }
         // 10.获取Mapper组件实际要执行的SQL
         String sql = boundSql.getSql();
         // 11.生成一条统计总记录数的SQL语句
         String countSql = "select count(*) from (" + sql + ") a";
         Connection connection = (Connection) invocation.getArgs()[0];
         PreparedStatement preparedStatement = connection
               .prepareStatement(countSql);
         // 12.获取ParameterHandler对象
         ParameterHandler parameterHandler = statementHandler.getParameterHandler();
//       // 也可通过如下代码利用MetaObject获取
//       var parameterHandler = (ParameterHandler) metaObject
//          .getValue("delegate.parameterHandler");   // ③
         // 为PreparedStatement中的SQL传入参数
         parameterHandler.setParameters(preparedStatement);
         ResultSet rs = preparedStatement.executeQuery();
         if (rs.next()) {
            int totalRec = rs.getInt(1);
            // 计算总页数
            page.setTotalPage(totalRec / page.getPageSize() == 0 ?
                  totalRec / page.getPageSize() : totalRec / page.getPageSize() + 1);
         }
         // 13.修改SQL语句,增加分页语法(只针对MySQL)
         String pageSql = sql + " limit "
               + (page.getPageIndex() - 1) * page.getPageSize() + ", "
               + page.getPageSize();
         // 14.改变Mapper方法实际要执行的SQL语句
         metaObject.setValue("delegate.boundSql.sql", pageSql);
      }
      return invocation.proceed();
   }
   @Override
   public Object plugin(Object o) {
      return Plugin.wrap(o, this);
   }
   @Override
   public void setProperties(Properties properties) {}
}

配置拦截器

<!-- 配置拦截器(分页插件) -->
<plugins>
   <plugin interceptor="com.xhxy.eshop.interceptor.PagePlugin" />
</plugins>