Mybatis 插件

1. 插件简介

一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易见的,一是增加了框架的灵活性。一是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作。MyBatis 为例,我们可基于 MyBatis插件机制实现分页、分表,监控等功能。由于插件和业务无关,业务也无法感知插件的存在。因此可以无感植入插件,在无形中增强功能。

2. Mybatis 插件介绍

Mybatis 作为⼀个应用广泛的优秀的 ORM 开源框架,这个框架具有强大的灵活性,在四大组件 (Executor、StatementHandler、ParameterHandler、ResultSetHandler) 处提供了简单易用的插件扩展机制。Mybatis 对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进行拦截,对 mybatis 来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象

MyBatis 所允许拦截的方法如下:

  • 执行器 Executor (updatequerycommit、rollback等方法);
  • SQL 语法构建器 StatementHandler (prepareparameterizebatchupdatesquery 等方法);
  • 参数处理器 ParameterHandler (getParameterObjectsetParameters 方法);
  • 结果集处理器 ResultSetHandler (handleResultSets``handleOutputParameters等方法);

3. Mybatis 插件原理

3.1 在四大对象创建的时候

  1. 每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(parameterHandler);
  2. 获取到所有的 Interceptor (拦截器),插件需要实现的接口;调用 interceptor.plugin(target) ;返回 target 包装后的对象
  3. 插件机制,我们可以使用插件为目标对象创建⼀个代理对象;AOP (面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每⼀个执行;

3.2 拦截

插件具体是如何拦截并附加额外的功能的呢?以 ParameterHandler来说

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

interceptorChain 保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的 target 就可以理解为mybatis中的四大对象。返回的 target 是被重重代理后的对象

如果我们想要拦截 Executorquery 方法,那么可以这样定义插件:

@Intercepts({
    @Signature(
    type = Executor.class,
    method = "query",
    args= {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
    )
}) 
public class ExeunplePlugin implements Interceptor {
 //省略逻辑
}

除此之外,我们还需将插件配置到 sqlMapConfig.xml 中。


    

这样 MyBatis 在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis 处于就绪状态。我们在执行 SQL时,需要先通过 DefaultSqlSessionFactory 创建 SqlSessionExecutor 实例会在创建 SqlSession 的过程中被创建, Executor 实例创建完毕后,MyBatis 会通过 JDK 动态代理为实例生成代理类。这样,插件逻辑即可在 Executor 相关方法被调用前执行。
以上就是MyBatis插件机制的基本原理

4. 自定义插件

4.1 插件接口

Mybatis 插件接口 Interceptor

  • Intercept 方法,插件的核心方法
  • plugin 方法,生成 target 的代理对象
  • setProperties 方法,传递插件所需参数

4.2 自定义插件

  • 设计实现一个自定义插件
package com.study.plugin;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;

import java.sql.Connection;
import java.util.Properties;

/**
 * 自定义插件
 * @author xiaosong
 * @since 2021/4/14
 */
@Intercepts({ //注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
        @Signature(
                type = StatementHandler.class,//这是指拦截哪个接口
                method = "prepare", //这个接口内的哪个方法名,不要拼错了
                args = {Connection.class,Integer.class} // 这是拦截的方法的⼊参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
        )
})
public class MyPlugin implements Interceptor {

    /**
     * //这里是每次执⾏操作的时候,都会进行这个拦截器的方法内
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //增强逻辑
        System.out.println("对方法进⾏了增强....");
        //执行原方法
        return invocation.proceed();
    }

    /**
     * 主要是为了把这个拦截器⽣成一个代理放到拦截器链中,包装⽬标对象 为⽬标对象创建代理对象
     * @param target 为要拦截的对象
     * @return 代理对象
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("将要包装的目标对象:"+target);
        return Plugin.wrap(target,this);
    }

    /**
     * 获取配置文件的属性
     * 插件初始化的时候调用,也只调用⼀次,插件配置的属性从这里设置进来
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数:"+properties);
    }
}
  • sqlMapConfig.xml

    
        
        
    

  • mapper 接口
public interface UserMapper {
    List selectUser();
}
  • 测试类
package com.study.mapper;

import com.study.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * 缓存的测试
 * @author xiaosong
 * @since 2021/4/12
 */
public class PluginTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("sqlMapperConfig.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testSelect(){
        //根据 sqlSessionFactory 产生 session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List userList = userMapper.findUserList();
        for (User user : userList) {
            System.out.println(user);
        }
    }

}
  • 执行顺序



5. 源码分析

  • 执行插件逻辑
    Plugin 实现了 InvocationHandler 接口,因此它的 invoke 方法会拦截所有的方法调用。invoke 方法会对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 获取被拦截方法列表,比如:signatureMap.get(Executor.class), 可能返回 [query, update,commit]
        Set methods = signatureMap.get(method.getDeclaringClass());
        //检测方法列表是否包含被拦截的方法
        if (methods != null && methods.contains(method)) {
            //执行插件逻辑
            return interceptor.intercept(new Invocation(target, method, args));
        }
        //执行被拦截的方法
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

invoke 方法的代码比较少,逻辑不难理解。首先, invoke 方法会检测被拦截方法是否配置在插件的 @Signature 注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在 intercept中,该方法的参数类型为 InvocationInvocation主要用于存储目标类,方法以及方法参数列表。下面简单看⼀下该类的定义

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author Clinton Begin
 */
public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    // 调用被拦截的方法
    return method.invoke(target, args);
  }

}

6. pageHelper 分页插件

MyBatis 可以使用第三方的插件来对功能进行扩展,分页助手
PageHelper 是将分的复杂操作进行封装,使用简单的方式即可获得分页的相关数据。

开发步骤:
① 导入通用 PageHelper 的坐标
② 在 mybatis 核心配置文件中配置 PageHelper 插件
③ 测试分页数据获取

代码实现:

  • 导入通用 PageHelper 坐标

    com.github.pagehelper
    pagehelper
    4.2.1

  • mybatis 核心配置文件中配置 PageHelper 插件

    
        
    

  • 测试分页代码实现
@Test
public void testPageHelper(){
    PageHelper.startPage(1,1);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List userList = userMapper.findUserList();
    for (User user : userList) {
        System.out.println(user);
    }
}
  • 获得分页相关的其他参数
@Test
public void testPageHelper(){
    PageHelper.startPage(1,1);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List userList = userMapper.findUserList();
    for (User user : userList) {
        System.out.println(user);
    }
    // 其它分页数据
    PageInfo pageInfo = new PageInfo<>(userList);
    System.out.println("总条数:"+pageInfo.getTotal());
    System.out.println("总⻚数:"+pageInfo. getPages ());
    System.out.println("当前⻚:"+pageInfo. getPageNum());
    System.out.println("每⻚显万⻓度:"+pageInfo.getPageSize());
    System.out.println("是否第⼀⻚:"+pageInfo.isIsFirstPage());
    System.out.println("是否最后⼀⻚:"+pageInfo.isIsLastPage());
}

7. 通用 Mapper

7.1 什么是通用 Mapper

通用 Mapper 就是为了解决单表增删改查,基于 Mybatis 的插件机制。开发人员不需要编写 SQL,不需要在 DAO 中增加方法,只要写好实体类,就能支持相应的增删改查方法

7.2 如何使用

  • 首先在 maven 项目,在 pom.xml 中引入 mapper 的依赖

    tk.mybatis
    mapper
    3.1.2

  • Mybatis 配置文件中完成配置

    
        
    

  • 实体类设置主键
package com.study.pojo;

import lombok.Data;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.List;

/**
 * 用户的实体类
 * @author xiaosong
 * @since 2021/4/1
 */
@Data
@Table(name = "user")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String username;

    private String password;

    private String birthday;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", birthday='" + birthday + '\'' +
                '}';
    }
}
  • 定义通用 mapper
package com.study.mapper;

import com.study.pojo.User;
import tk.mybatis.mapper.common.Mapper;

import java.util.List;

/**
 * 用户的持久层
 * @author xiaosong
 * @since 2021/4/12
 */
public interface UserMapper extends Mapper {

    /**
     * 查询用户信息
     * @return List
     */
    List findUserList();

}
  • 测试
@Test
public void testMapper(){
    SqlSession sqlSession = sqlSessionFactory.openSession(true);
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = new User();
    user.setId(1);

    // mapper 基础接口, select
    // 根据实体中的属性进行查询,只能有一个返回值
    User user1 = userMapper.selectOne(user);
    System.out.println(user1);
    // 参数为 null 时,查询全部
    List userList = userMapper.select(null);
    for (User user2 : userList) {
        System.out.println(user2);
    }

    // 根据主键字段进行查询,方法参数必须包含完整的主键属性,查询条件使用等号
    User user2 = userMapper.selectByPrimaryKey(1);
    System.out.println(user2);

    //根据实体中的属性查询总数,查询条件使用等号
    int count = userMapper.selectCount(user);
    System.out.println("总数" + count);


    // mapper 基础接口, insert
    //保存一个实体,null值也会保存,不会使用数据库默认值
    user.setId(3);
    int insertCount = userMapper.insert(user);
    System.out.println("插入条数" + insertCount);

    //保存实体,null的属性不会保存,会使用数据库默认值
    user.setId(4);
    int insertCount1 = userMapper.insertSelective(user);
    System.out.println("插入条数" + insertCount1);


    // mapper 基础接口, update
    //根据主键更新实体全部字段, null值会被更新
    user.setId(4);
    int updateCount = userMapper.updateByPrimaryKey(user);
    System.out.println("更新条数" + updateCount);



    // mapper 基础接口, delete
    //根据实体属性作为条件进行删除,查询条件使用等号
    int deleteCount = userMapper.delete(user);
    System.out.println("删除条数" + deleteCount);


    // example方法
    Example example = new Example(User.class);
    example.createCriteria().andEqualTo("id",1)
            .andLike("username","E%");
    List userList1 = userMapper.selectByExample(example);
    for (User user3 : userList1) {
        System.out.println(user3);
    }
}

8. 案例代码

详情参考:https://gitee.com/xiaosonglab/mybatis-plugins.git

你可能感兴趣的:(Mybatis 插件)