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 中。

<plugins>
    <plugin interceptor="com.lagou.plugin.ExamplePlugin"/>
</plugins>

这样 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
<plugins>
    <plugin interceptor="com.study.plugin.MyPlugin">
        <!--配置参数-->
        <property name="name" value="Bob"/>
    </plugin>
</plugins>
  • mapper 接口
public interface UserMapper {
    List<User> 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<User> 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<Method> 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 坐标
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>4.2.1</version>
</dependency>
  • mybatis 核心配置文件中配置 PageHelper 插件
<plugins>
    <plugin interceptor="com.github.pagehelper.PageHelper">
        <property name="dialect" value="mysql"/>
    </plugin>
</plugins>
  • 测试分页代码实现
@Test
public void testPageHelper(){
    PageHelper.startPage(1,1);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    List<User> 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<User> userList = userMapper.findUserList();
    for (User user : userList) {
        System.out.println(user);
    }
    // 其它分页数据
    PageInfo<User> 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 的依赖
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>3.1.2</version>
</dependency>
  • Mybatis 配置文件中完成配置
<plugins>
    <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
        <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
    </plugin>
</plugins>
  • 实体类设置主键
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<User> {

    /**
     * 查询用户信息
     * @return List<User>
     */
    List<User> 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<User> 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<User> userList1 = userMapper.selectByExample(example);
    for (User user3 : userList1) {
        System.out.println(user3);
    }
}

8. 案例代码

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,561评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,202评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,569评论 2 7