MyBatis--插件开发

MyBatis--插件开发

十、MyBatis-插件开发

MyBatis在四大对象的创建过程中,都会有插件进行 介入。插件可以利用动态代理机制一层层的包装目标对象,而实现在目标对象执行目标方法之前进行拦截 的效果。
MyBatis 允许在已映射语句执行过程中的某一点进行 拦截调用。
默认情况下,MyBatis 允许使用插件来拦截的方法调 用包括:

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

10.1、插件开发

插件开发步骤
1)、编写插件实现Interceptor接口,并使用 @Intercepts注解完成插件签名。
2)、在全局配置文件中注册插件。

/**
 * 完成插件签名:
 *      告诉MyBatis当前插件用来拦截哪个对象的哪个方法
 */
@Intercepts(
        {
            @Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
        })
public class MyFirstPlugin implements Interceptor{

    /**
     * intercept:拦截:
     *      拦截目标对象的目标方法的执行;
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
        //动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询3号员工
        Object target = invocation.getTarget();
        System.out.println("当前拦截到的对象:"+target);
        //拿到:StatementHandler==>ParameterHandler===>parameterObject
        //拿到target的元数据
        MetaObject metaObject = SystemMetaObject.forObject(target);
        Object value = metaObject.getValue("parameterHandler.parameterObject");
        System.out.println("sql语句用的参数是:"+value);
        //修改完sql语句要用的参数
        metaObject.setValue("parameterHandler.parameterObject", 3);
        //执行目标方法
        Object proceed = invocation.proceed();
        //返回执行后的返回值
        return proceed;
    }

    /**
     * plugin:
     *      包装目标对象的:包装:为目标对象创建一个代理对象
     */
    @Override
    public Object plugin(Object target) {
        // TODO Auto-generated method stub
        //我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
        System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
        Object wrap = Plugin.wrap(target, this);
        //返回为当前target创建的动态代理
        return wrap;
    }

    /**
     * setProperties:
     *      将插件注册时 的property属性设置进来
     */
    @Override
    public void setProperties(Properties properties) {
        // TODO Auto-generated method stub
        System.out.println("插件配置的信息:"+properties);
    }
}

测试代码:

/**
 * 1、获取sqlSessionFactory对象:
 *      解析文件的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession;
 *      注意:【MappedStatement】:代表一个增删改查的详细信息
 *  
 * 2、获取sqlSession对象
 *      返回一个DefaultSQlSession对象,包含Executor和Configuration;
 *      这一步会创建Executor对象;
 * 
 * 3、获取接口的代理对象(MapperProxy)
 *      getMapper,使用MapperProxyFactory创建一个MapperProxy的代理对象
 *      代理对象里面包含了,DefaultSqlSession(Executor)
 * 4、执行增删改查方法
 * 
 * 总结:
 *  1、根据配置文件(全局,sql映射)初始化出Configuration对象
 *  2、创建一个DefaultSqlSession对象,
 *      他里面包含Configuration以及
 *      Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
 *  3、DefaultSqlSession.getMapper():拿到Mapper接口对应的MapperProxy;
 *  4、MapperProxy里面有(DefaultSqlSession);
 *  5、执行增删改查方法:
 *          1)、调用DefaultSqlSession的增删改查(Executor);
 *          2)、会创建一个StatementHandler对象。
 *              (同时也会创建出ParameterHandler和ResultSetHandler)
 *          3)、调用StatementHandler预编译参数以及设置参数值;
 *              使用ParameterHandler来给sql设置参数
 *          4)、调用StatementHandler的增删改查方法;
 *          5)、ResultSetHandler封装结果
 *  注意:
 *      四大对象每个创建的时候都有一个interceptorChain.pluginAll(parameterHandler);
 * 
 * @throws IOException
 */
@Test
public void test01() throws IOException {
    // 1、获取sqlSessionFactory对象
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    // 2、获取sqlSession对象
    SqlSession openSession = sqlSessionFactory.openSession();
    try {
        // 3、获取接口的实现类对象
        //会为接口自动的创建一个代理对象,代理对象去执行增删改查方法
        EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
        Employee employee = mapper.getEmpById(1);
        System.out.println(mapper);
        System.out.println(employee);
    } finally {
        openSession.close();
    }
}
/**
 * 插件原理
 * 在四大对象创建的时候
 * 1、每个创建出来的对象不是直接返回的,而是
 *      interceptorChain.pluginAll(parameterHandler);
 * 2、获取到所有的Interceptor(拦截器)(插件需要实现的接口);
 *      调用interceptor.plugin(target);返回target包装后的对象
 * 3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面向切面)
 *      我们的插件可以为四大对象创建出代理对象;
 *      代理对象就可以拦截到四大对象的每一个执行;
 * 
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          target = interceptor.plugin(target);
        }
        return target;
      }
 */

全局配置文件mybatis-config.xml中配置注册 插件

<!--plugins:注册插件  -->
<plugins>
    <plugin interceptor="com.atguigu.mybatis.dao.MyFirstPlugin">
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </plugin>
    <plugin interceptor="com.atguigu.mybatis.dao.MySecondPlugin"></plugin>
</plugins>

输出结果:

插件配置的信息:{password=root, username=root}
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@56ac3a89
MySecondPlugin...plugin:org.apache.ibatis.executor.CachingExecutor@56ac3a89
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@3632be31
MySecondPlugin...plugin:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@3632be31
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@13a5fe33
MySecondPlugin...plugin:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@13a5fe33
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@370736d9
MySecondPlugin...plugin:org.apache.ibatis.executor.statement.RoutingStatementHandler@370736d9
DEBUG 09-29 21:10:52,348 ==>  Preparing: select id,last_name lastName,email,gender from tbl_employee where id = ?   (BaseJdbcLogger.java:145) 
MySecondPlugin...intercept:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
MyFirstPlugin...intercept:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
当前拦截到的对象:org.apache.ibatis.executor.statement.RoutingStatementHandler@370736d9
sql语句用的参数是:1
DEBUG 09-29 21:10:52,372 ==> Parameters: 3(Integer)  (BaseJdbcLogger.java:145) 
DEBUG 09-29 21:10:52,391 <==      Total: 1  (BaseJdbcLogger.java:145) 
org.apache.ibatis.binding.MapperProxy@33723e30
Employee [id=3, lastName=jerry4, email=null, gender=1]

10.2、插件原理

  • 1)、按照插件注解声明,按照插件配置顺序调用插件plugin方 法,生成被拦截对象的动态代理
  • 2)、多个插件依次生成目标对象的代理对象,层层包裹,先声 明的先包裹;形成代理链
  • 3)、目标方法执行时依次从外到内执行插件的intercept方法。
  • 4)、多个插件情况下,我们往往需要在某个插件中分离出目标对象。可以借助MyBatis提供的SystemMetaObject类来进行获 取最后一层的h以及target属性的值。

10.3、Interceptor接口

Intercept:拦截目标方法执行。
plugin:生成动态代理对象,可以使用MyBatis提 供的Plugin类的wrap方法。
setProperties:注入插件配置时设置的属性。

十一、扩展:MyBatis实用场景

  • 1)、PageHelper插件进行分页
  • 2)、批量操作
  • 3)、存储过程
  • 4)、typeHandler处理枚举

11.1、PageHelper插件进行分页
PageHelper是MyBatis中非常方便的第三方分页 插件。
官方文档:

https://github.com/pagehelper/MybatisPageHelper/blob/master/README_zh.md

使用步骤:

  • 1、导入相关包pagehelper-x.x.x.jar 和 jsqlparser0.9.5.jar。
  • 2、在MyBatis全局配置文件中配置分页插件。
  • 3、使用PageHelper提供的方法进行分页。
  • 4、可以使用更强大的PageInfo封装返回结果。

11.2、批量操作

默认的 openSession() 方法没有参数,它会创建有如下特性的会开启一个事务(也就是不自动提交) 连接对象会从由活动环境配置的数据源实例得到。 事务隔离级别将会使用驱动或数据源的默认设置。 预处理语句不会被复用,也不会批量处理更新。
openSession 方法的 ExecutorType类型的参数,枚举类型:

ExecutorType.SIMPLE: 这个执行器类型不做特殊的事情(这是默认装配 的)。它为每个语句的执行创建一个新的预处理语句。 
ExecutorType.REUSE: 这个执行器类型会复用预处理语句。
ExecutorType.BATCH: 这个执行器会批量执行所有更新语句。

批量操作我们是使用MyBatis提供的BatchExecutor进行的,他的底层就是通过jdbc攒sql的方式进行的。我们可以让他攒够一定数量后发给数据库一次。

@Test
public void testBatch() throws IOException{
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    
    //可以执行批量操作的sqlSession
    SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
    long start = System.currentTimeMillis();
    try{
        EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
        for (int i = 0; i < 10000; i++) {
            mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
        }
        openSession.commit();
        long end = System.currentTimeMillis();
        //批量:(预编译sql一次==>设置参数===>10000次===>执行(1次))
        //Parameters: 616c1(String), b(String), 1(String)==>4598
        //非批量:(预编译sql=设置参数=执行)==》10000    10200
        System.out.println("执行时长:"+(end-start));
    }finally{
        openSession.close();
    }
}

与Spring整合中,我们推荐,额外的配置一个可以专门用来执行批量操作的sqlSession。

需要用到批量操作的时候,我们可以注入配置的这个批量 SqlSession。通过他获取到mapper映射器进行操作。

注意:
1、批量操作是在session.commit()以后才发送sql语句给数、据库进行执行的。
2、如果我们想让其提前执行,以方便后续可能的查询操作 获取数据,我们可以使用sqlSession.flushStatements()方法,让其直接冲刷到数据库进行执行。

11.3、存储过程

实际开发中,我们通常也会写一些存储过程, MyBatis也支持对存储过程的调用。
一个最简单的存储过程:

delimiter $$ 
create procedure test() 
begin 
    select 'hello'; 
end $$ 
delimiter ; 

存储过程的调用
1、select标签中

statementType=“CALLABLE” 

2、标签体中调用语法:

{call procedure_name(#{param1_info},#{param2_info})}

存储过程过-游标处理
MyBatis对存储过程的游标提供了一个JdbcType=CURSOR的支持, 可以智能的把游标读取到的数据,映射到我们声明的结果集中。
调用实例:

<!-- public void getPageByProcedure(); 
1、使用select标签定义调用存储过程
2、statementType="CALLABLE":表示要调用存储过程
3、{call procedure_name(params)}
-->
<select id="getPageByProcedure" statementType="CALLABLE" databaseId="oracle">
    {call hello_test(
         #{start,mode=IN,jdbcType=INTEGER},
         #{end,mode=IN,jdbcType=INTEGER},
        #{count,mode=OUT,jdbcType=INTEGER},
        #{emps,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=PageEmp}
    )}
</select>
<resultMap type="com.atguigu.mybatis.bean.Employee" id="PageEmp">
    <id column="EMPLOYEE_ID" property="id"/>
    <result column="LAST_NAME" property="email"/>
    <result column="EMAIL" property="email"/>
</resultMap>

测试代码:

/**
 * oracle分页:
 *      借助rownum:行号;子查询;
 * 存储过程包装分页逻辑
 * @throws IOException 
 */
@Test
public void testProcedure() throws IOException{
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    SqlSession openSession = sqlSessionFactory.openSession();
    try{
        EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
        OraclePage page = new OraclePage();
        page.setStart(1);
        page.setEnd(5);
        mapper.getPageByProcedure(page);
        
        System.out.println("总记录数:"+page.getCount());
        System.out.println("查出的数据:"+page.getEmps().size());
        System.out.println("查出的数据:"+page.getEmps());
    }finally{
        openSession.close();
    }
}

OraclePage:

/**
 * 封装分页查询数据
 * @author lfy
 *
 */
public class OraclePage {
    
    private int start;
    private int end;
    private int count;
    private List<Employee> emps;
 }

存储过程:

create or replace procedure
    hello_test(p_start in int,
    p_end in int,
    P_count out int,
    ref_cur out sys_refcursor) AS
BEGIN
    select count(*) into p_count from emp;
    open ref_cur for
    select * from (select e.*,rownum as r1 from emp e where rownum<p_end)where r1>p_start;
END hello_test;

11.4、自定义TypeHandler处理枚举

可以通过自定义TypeHandler的形式来在设置参数或 者取出结果集的时候自定义参数封装策略。
步骤:

  • 1、实现TypeHandler接口或者继承BaseTypeHandler。
  • 2、使用@MappedTypes定义处理的java类型 使用@MappedJdbcTypes定义jdbcType类型。
  • 3、在自定义结果集标签或者参数处理的时候声明使用自定义 TypeHandler进行处理 或者在全局配置TypeHandler要处理的javaType。

测试实例
1、一个枚举类

/**
 * 希望数据库保存的是100,200这些状态码,而不是默认0,1或者枚举的名
 * @author lfy
 *
 */
public enum EmpStatus {
    LOGIN(100,"用户登录"),LOGOUT(200,"用户登出"),REMOVE(300,"用户不存在");
    
    
    private Integer code;
    private String msg;
    private EmpStatus(Integer code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public Integer getCode() {
        return code;
    }
    
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    
    //按照状态码返回枚举对象
    public static EmpStatus getEmpStatusByCode(Integer code){
        switch (code) {
            case 100:
                return LOGIN;
            case 200:
                return LOGOUT;  
            case 300:
                return REMOVE;
            default:
                return LOGOUT;
        }
    }
}

2、配置

<typeHandlers>
    <!--1、配置我们自定义的TypeHandler  -->
    <typeHandler handler="com.atguigu.mybatis.typehandler.MyEnumEmpStatusTypeHandler" javaType="com.atguigu.mybatis.bean.EmpStatus"/>
    <!--2、也可以在处理某个字段的时候告诉MyBatis用什么类型处理器
            保存:#{empStatus,typeHandler=xxxx}
            查询:
                <resultMap type="com.atguigu.mybatis.bean.Employee" id="MyEmp">
                    <id column="id" property="id"/>
                    <result column="empStatus" property="empStatus" typeHandler=""/>
                </resultMap>
            注意:如果在参数位置修改TypeHandler,应该保证保存数据和查询数据用的TypeHandler是一样的。
      -->
</typeHandlers>

3、测试代码

/**
 * 1、实现TypeHandler接口。或者继承BaseTypeHandler
 * @author lfy
 */
public class MyEnumEmpStatusTypeHandler implements TypeHandler<EmpStatus> {

    /**
     * 定义当前数据如何保存到数据库中
     */
    @Override
    public void setParameter(PreparedStatement ps, int i, EmpStatus parameter,
            JdbcType jdbcType) throws SQLException {
        // TODO Auto-generated method stub
        System.out.println("要保存的状态码:"+parameter.getCode());
        ps.setString(i, parameter.getCode().toString());
    }

    @Override
    public EmpStatus getResult(ResultSet rs, String columnName)
            throws SQLException {
        // TODO Auto-generated method stub
        //需要根据从数据库中拿到的枚举的状态码返回一个枚举对象
        int code = rs.getInt(columnName);
        System.out.println("从数据库中获取的状态码:"+code);
        EmpStatus status = EmpStatus.getEmpStatusByCode(code);
        return status;
    }

    @Override
    public EmpStatus getResult(ResultSet rs, int columnIndex)
            throws SQLException {
        // TODO Auto-generated method stub
        int code = rs.getInt(columnIndex);
        System.out.println("从数据库中获取的状态码:"+code);
        EmpStatus status = EmpStatus.getEmpStatusByCode(code);
        return status;
    }

    @Override
    public EmpStatus getResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        // TODO Auto-generated method stub
        int code = cs.getInt(columnIndex);
        System.out.println("从数据库中获取的状态码:"+code);
        EmpStatus status = EmpStatus.getEmpStatusByCode(code);
        return status;
    }
}

测试代码:

/**
 * 默认mybatis在处理枚举对象的时候保存的是枚举的名字:EnumTypeHandler
 * 改变使用:EnumOrdinalTypeHandler:
 * @throws IOException
 */
@Test
public void testEnum() throws IOException{
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    SqlSession openSession = sqlSessionFactory.openSession();
    try{
        EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
        Employee employee = new Employee("test_enum", "enum@atguigu.com","1");
        mapper.addEmp(employee);
        System.out.println("保存成功"+employee.getId());
        openSession.commit();
        
//          Employee empById = mapper.getEmpById(30026);
//          System.out.println(empById.getEmpStatus());
    }finally{
        openSession.close();
    }
}

评论

暂无

添加新评论