Mybatis 插件

1. 插件簡介

一般情況下,開源框架都會提供插件或其他形式的拓展點雷逆,供開發(fā)者自行拓展污尉。這樣的好處是顯而易見的往产,一是增加了框架的靈活性某宪。一是開發(fā)者可以結合實際需求兴喂,對框架進行拓展衣迷,使其能夠更好的工作。MyBatis 為例云矫,我們可基于 MyBatis插件機制實現(xiàn)分頁让禀、分表陨界,監(jiān)控等功能菌瘪。由于插件和業(yè)務無關,業(yè)務也無法感知插件的存在缀皱。因此可以無感植入插件啤斗,在無形中增強功能赁咙。

2. Mybatis 插件介紹

Mybatis 作為?個應用廣泛的優(yōu)秀的 ORM 開源框架彼水,這個框架具有強大的靈活性凤覆,在四大組件 (Executor、StatementHandler慈俯、ParameterHandler贴膘、ResultSetHandler) 處提供了簡單易用的插件擴展機制。Mybatis 對持久層的操作就是借助于四大核心對象洋闽。MyBatis支持用插件對四大核心對象進行攔截诫舅,對 mybatis 來說插件就是攔截器骚勘,用來增強核心對象的功能撮奏,增強功能本質(zhì)上是借助于底層的動態(tài)代理實現(xiàn)的畜吊,換句話說玲献,MyBatis中的四大對象都是代理對象梯浪。

MyBatis 所允許攔截的方法如下:

  • 執(zhí)行器 Executor (update挂洛、query虏劲、commit柒巫、rollback等方法);
  • SQL 語法構建器 StatementHandler (prepare应结、parameterize鹅龄、batchupdates漩绵、query 等方法)止吐;
  • 參數(shù)處理器 ParameterHandler (getParameterObject碍扔、setParameters 方法)秕重;
  • 結果集處理器 ResultSetHandler (handleResultSets``handleOutputParameters等方法)溶耘;

3. Mybatis 插件原理

3.1 在四大對象創(chuàng)建的時候

  1. 每個創(chuàng)建出來的對象不是直接返回的凳兵,而是interceptorChain.pluginAll(parameterHandler);
  2. 獲取到所有的 Interceptor (攔截器)庐扫,插件需要實現(xiàn)的接口;調(diào)用 interceptor.plugin(target) 铅辞;返回 target 包裝后的對象
  3. 插件機制斟珊,我們可以使用插件為目標對象創(chuàng)建?個代理對象验靡;AOP (面向切面)我們的插件可以為四大對象創(chuàng)建出代理對象胜嗓,代理對象就可以攔截到四大對象的每?個執(zhí)行辞州;

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初始化的時候創(chuàng)建的阔拳。調(diào)用攔截器鏈中的攔截器依次的對目標進行攔截或增強类嗤。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 處于就緒狀態(tài)酪劫。我們在執(zhí)行 SQL時寺董,需要先通過 DefaultSqlSessionFactory 創(chuàng)建 SqlSession遮咖。Executor 實例會在創(chuàng)建 SqlSession 的過程中被創(chuàng)建, Executor 實例創(chuàng)建完畢后漓藕,MyBatis 會通過 JDK 動態(tài)代理為實例生成代理類挟裂。這樣诀蓉,插件邏輯即可在 Executor 相關方法被調(diào)用前執(zhí)行交排。
以上就是MyBatis插件機制的基本原理

4. 自定義插件

4.1 插件接口

Mybatis 插件接口 Interceptor

  • Intercept 方法埃篓,插件的核心方法
  • plugin 方法架专,生成 target 的代理對象
  • setProperties 方法,傳遞插件所需參數(shù)

4.2 自定義插件

  • 設計實現(xiàn)一個自定義插件
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", //這個接口內(nèi)的哪個方法名锡移,不要拼錯了
                args = {Connection.class,Integer.class} // 這是攔截的方法的?參淆珊,按順序?qū)懙竭@施符,不要多也不要少戳吝,如果方法重載,可是要通過方法名和入?yún)泶_定唯一的
        )
})
public class MyPlugin implements Interceptor {

    /**
     * //這里是每次執(zhí)?操作的時候柬采,都會進行這個攔截器的方法內(nèi)
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //增強邏輯
        System.out.println("對方法進?了增強....");
        //執(zhí)行原方法
        return invocation.proceed();
    }

    /**
     * 主要是為了把這個攔截器?成一個代理放到攔截器鏈中粉捻,包裝?標對象 為?標對象創(chuàng)建代理對象
     * @param target 為要攔截的對象
     * @return 代理對象
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("將要包裝的目標對象:"+target);
        return Plugin.wrap(target,this);
    }

    /**
     * 獲取配置文件的屬性
     * 插件初始化的時候調(diào)用斑芜,也只調(diào)用?次杏头,插件配置的屬性從這里設置進來
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化參數(shù):"+properties);
    }
}
  • sqlMapConfig.xml
<plugins>
    <plugin interceptor="com.study.plugin.MyPlugin">
        <!--配置參數(shù)-->
        <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(){
        //根據(jù) sqlSessionFactory 產(chǎn)生 session
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> userList = userMapper.findUserList();
        for (User user : userList) {
            System.out.println(user);
        }
    }

}
  • 執(zhí)行順序



5. 源碼分析

  • 執(zhí)行插件邏輯
    Plugin 實現(xiàn)了 InvocationHandler 接口,因此它的 invoke 方法會攔截所有的方法調(diào)用叛氨。invoke 方法會對所攔截的方法進行檢測寞埠,以決定是否執(zhí)行插件邏輯仁连。該方法的邏輯如下:
@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)) {
            //執(zhí)行插件邏輯
            return interceptor.intercept(new Invocation(target, method, args));
        }
        //執(zhí)行被攔截的方法
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

invoke 方法的代碼比較少,邏輯不難理解颇象。首先, invoke 方法會檢測被攔截方法是否配置在插件的 @Signature 注解中伍伤,若是,則執(zhí)行插件邏輯遣钳,否則執(zhí)行被攔截方法。插件邏輯封裝在 intercept中麦乞,該方法的參數(shù)類型為 Invocation蕴茴,Invocation主要用于存儲目標類,方法以及方法參數(shù)列表姐直。下面簡單看?下該類的定義

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 {
    // 調(diào)用被攔截的方法
    return method.invoke(target, args);
  }

}

6. pageHelper 分頁插件

MyBatis 可以使用第三方的插件來對功能進行擴展,分頁助手
PageHelper 是將分的復雜操作進行封裝声畏,使用簡單的方式即可獲得分頁的相關數(shù)據(jù)撞叽。

開發(fā)步驟:
① 導入通用 PageHelper 的坐標
② 在 mybatis 核心配置文件中配置 PageHelper 插件
③ 測試分頁數(shù)據(jù)獲取

代碼實現(xiàn):

  • 導入通用 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>
  • 測試分頁代碼實現(xiàn)
@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);
    }
}
  • 獲得分頁相關的其他參數(shù)
@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);
    }
    // 其它分頁數(shù)據(jù)
    PageInfo<User> pageInfo = new PageInfo<>(userList);
    System.out.println("總條數(shù):"+pageInfo.getTotal());
    System.out.println("總?數(shù):"+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 的插件機制。開發(fā)人員不需要編寫 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
    // 根據(jù)實體中的屬性進行查詢才睹,只能有一個返回值
    User user1 = userMapper.selectOne(user);
    System.out.println(user1);
    // 參數(shù)為 null 時,查詢?nèi)?    List<User> userList = userMapper.select(null);
    for (User user2 : userList) {
        System.out.println(user2);
    }

    // 根據(jù)主鍵字段進行查詢甘邀,方法參數(shù)必須包含完整的主鍵屬性琅攘,查詢條件使用等號
    User user2 = userMapper.selectByPrimaryKey(1);
    System.out.println(user2);

    //根據(jù)實體中的屬性查詢總數(shù),查詢條件使用等號
    int count = userMapper.selectCount(user);
    System.out.println("總數(shù)" + count);


    // mapper 基礎接口松邪, insert
    //保存一個實體坞琴,null值也會保存,不會使用數(shù)據(jù)庫默認值
    user.setId(3);
    int insertCount = userMapper.insert(user);
    System.out.println("插入條數(shù)" + insertCount);

    //保存實體逗抑,null的屬性不會保存剧辐,會使用數(shù)據(jù)庫默認值
    user.setId(4);
    int insertCount1 = userMapper.insertSelective(user);
    System.out.println("插入條數(shù)" + insertCount1);


    // mapper 基礎接口, update
    //根據(jù)主鍵更新實體全部字段锋八, null值會被更新
    user.setId(4);
    int updateCount = userMapper.updateByPrimaryKey(user);
    System.out.println("更新條數(shù)" + updateCount);



    // mapper 基礎接口浙于, delete
    //根據(jù)實體屬性作為條件進行刪除,查詢條件使用等號
    int deleteCount = userMapper.delete(user);
    System.out.println("刪除條數(shù)" + 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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挟纱,一起剝皮案震驚了整個濱河市羞酗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌紊服,老刑警劉巖檀轨,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異欺嗤,居然都是意外死亡参萄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門煎饼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讹挎,“玉大人,你說我怎么就攤上這事吆玖⊥怖#” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵沾乘,是天一觀的道長怜奖。 經(jīng)常有香客問我,道長翅阵,這世上最難降的妖魔是什么歪玲? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任迁央,我火速辦了婚禮,結果婚禮上滥崩,老公的妹妹穿的比我還像新娘岖圈。我一直安慰自己,他們只是感情好夭委,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布幅狮。 她就那樣靜靜地躺著,像睡著了一般株灸。 火紅的嫁衣襯著肌膚如雪崇摄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天慌烧,我揣著相機與錄音逐抑,去河邊找鬼。 笑死屹蚊,一個胖子當著我的面吹牛厕氨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播汹粤,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼命斧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嘱兼?” 一聲冷哼從身側響起国葬,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎芹壕,沒想到半個月后汇四,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡踢涌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年通孽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片睁壁。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡背苦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出潘明,到底是詐尸還是另有隱情糠惫,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布钉疫,位于F島的核電站,受9級特大地震影響巢价,放射性物質(zhì)發(fā)生泄漏牲阁。R本人自食惡果不足惜固阁,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望城菊。 院中可真熱鬧备燃,春花似錦、人聲如沸凌唬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽客税。三九已至况褪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間更耻,已是汗流浹背测垛。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留秧均,地道東北人食侮。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像目胡,于是被迫代替她去往敵國和親锯七。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 今天感恩節(jié)哎誉己,感謝一直在我身邊的親朋好友眉尸。感恩相遇!感恩不離不棄巫延。 中午開了第一次的黨會效五,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,573評論 0 11
  • 彩排完,天已黑
    劉凱書法閱讀 4,226評論 1 3
  • 表情是什么炉峰,我認為表情就是表現(xiàn)出來的情緒畏妖。表情可以傳達很多信息。高興了當然就笑了疼阔,難過就哭了戒劫。兩者是相互影響密不可...
    Persistenc_6aea閱讀 125,345評論 2 7