Mybatis插件介绍及原理分析

一. 插件简介

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

二. Mybatis插件介绍

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

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

执⾏器 Executor (update query commit rollback 等⽅法 )
SQL 语法构建器 StatementHandler (prepare parameterize batch updates query 等⽅ 法 )
参数处理器 ParameterHandler (getParameterObject setParameters ⽅法 )
结果集处理器 ResultSetHandler (handleResultSets handleOutputParameters 等⽅法 )

三. Mybatis插件原理

在四⼤对象创建的时候
1 、每个创建出来的对象不是直接返回的,⽽是 interceptorChain.pluginAll(parameterHandler);
2 、获取到所有的 Interceptor ( 拦截器 )( 插件需要实现的接⼝ ) ;调⽤ interceptor.plugin(target); 返回 target 包 装后的对象
3 、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象; AOP ( ⾯向切⾯ ) 我们的插件可 以为四⼤对象 创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;
拦截
插件具体是如何拦截并附加额外的功能的呢?以 ParameterHandler 来说
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
                                                Object object, BoundSql sql, InterceptorChain interceptorChain){
        ParameterHandler parameterHandler =
                mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
        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 是被重重代理后的对象
如果我们想要拦截 Executor query ⽅法,那么可以这样定义插件:
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
        )
})
public class ExeunplePlugin implements Interceptor {
    //省略逻辑
}
除此之外,我们还需将插件配置到 sqlMapConfig.xm l 中。
<plugins>
 <plugin interceptor="com.lagou.plugin.ExamplePlugin">
 </plugin>
</plugins>
这样 MyBatis 在启动时可以加载插件,并保存插件实例到相关对象 (InterceptorChain ,拦截器链 ) 中。待准备⼯作 做完后,MyBatis 处于就绪状态。我们在执⾏ SQL 时,需要先通过 DefaultSqlSessionFactory 创建 SqlSession
Executor 实例会在创建 SqlSession 的过程中被创建, Executor 实例创建完毕后, MyBatis 会通过 JDK 动态代理为 实例⽣成代理类。这样,插件逻辑即可在 Executor 相关⽅法被调⽤前执⾏。
以上就是 MyBatis 插件机制的基本原理。

四. ⾃定义插件

(一)插件接⼝

Mybatis 插件接⼝ -Interceptor
• Intercept ⽅法,插件的核⼼⽅法
• plugin ⽅法,⽣成 target 的代理对象
• setProperties ⽅法,传递插件所需参数

(二)⾃定义插件

设计实现⼀个⾃定义插件
package com.lagou.plugin;

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

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

@Intercepts({
        @Signature(type= StatementHandler.class,
                  method = "prepare",
                  //args =后面这个大刮号中主要是填写prepare方法中的参数,以避免同名的重载方法。
                  args = {Connection.class,Integer.class})
})
public class MyPlugin implements Interceptor {

    /*
        拦截方法:只要被拦截的目标对象的目标方法被执行时,每次都会执行intercept方法(这个方法后执行)
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("对方法进行了增强....");
        return invocation.proceed(); //这里返回invocation.proceed()是指增强逻辑执行后,按照原方法执行
    }

    /*
       主要为了把当前的拦截器生成代理存到拦截器链中,(这个方法先执行)
     */
    @Override
    public Object plugin(Object target) {
        Object wrap = Plugin.wrap(target, this);//这里的target代表被拦截对象
        return wrap;
    }

    /*
        获取配置文件的参数,主要是获取在SqlMapConfig配置文件中配置插件时所设置的一些参数
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("获取到的配置文件的参数是:"+properties);
    }
}

 sqlMapConfig.xml 配置

<plugins>
 <plugin interceptor="com.lagou.plugin.plugin">
 <!--配置参数-->
 <property name="name" value="Bob"/>
 </plugin>
</plugins>

mapper接⼝

public interface UserMapper {
 List<User> selectUser();
}

mapper.xml

<mapper namespace="com.lagou.mapper.UserMapper">
 <select id="selectUser" resultType="com.lagou.pojo.User">
 SELECT
 id,username
 FROM
 user
 </select>
</mapper>

测试类

public class PluginTest {
    @Test
    public void test() throws IOException {
        InputStream resourceAsStream =
                Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new
                SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> byPaging = userMapper.selectUser();
        for (User user : byPaging) {
            System.out.println(user);
        }
    } }

五. 源码分析

执⾏插件逻辑

Plugin 实现了 InvocationHandler 接⼝,因此它的 invoke ⽅法会拦截所有的⽅法调⽤。 invoke ⽅法会 对所拦截的⽅ 法进⾏检测,以决定是否执⾏插件逻辑。该⽅法的逻辑如下:
    // -Plugin
    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){
            }
        }
invoke ⽅法的代码⽐较少,逻辑不难理解。⾸先 ,invoke ⽅法会检测被拦截⽅法是否配置在插件的 @Signature 注解 中,若是,则执⾏插件逻辑,否则执⾏被拦截⽅法。插件逻辑封装在intercept 中,该 ⽅法的参数类型为 Invocationo Invocation主要⽤于存储⽬标类,⽅法以及⽅法参数列表。下⾯简单看 ⼀下该类的定义
public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;
    public Invocation(Object targetf Method method, Object[] args) {
        this.target = target;
        this.method = method;
//省略部分代码
        public Object proceed() throws InvocationTargetException, IllegalAccessException { //调
⽤被拦截的⽅法
                    >> —
关于插件的执⾏逻辑就分析结束

六. pageHelper分⻚插件

MyBati s 可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿ PageHelper 是将分⻚的复杂操作进⾏封 装,使⽤简 单的⽅式即可获得分⻚的相关数据
开发步骤:
导⼊通⽤ PageHelper 的坐标
mybatis 核⼼配置⽂件中配置 PageHelper 插件
测试分⻚数据获取

(一)导⼊通⽤PageHelper坐标

<dependency>
 <groupId>com.github.pagehelper</groupId>
 <artifactId>pagehelper</artifactId>
 <version>3.7.5</version>
 </dependency>
 <dependency>
 <groupId>com.github.jsqlparser</groupId>
 <artifactId>jsqlparser</artifactId>
 <version>0.9.1</version>
 </dependency>

(二)mybatis核⼼配置⽂件中配置PageHelper插件

 <!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->*
 <plugin interceptor="com.github.pagehelper.PageHelper">
 <!—指定⽅⾔ —>
 <property name="dialect" value="mysql"/>
 </plugin>

(三)测试分⻚代码实现

    @Test
    public void testPageHelper() {
        //设置分⻚参数
        PageHelper.startPage(1, 2);
        List<User> select = userMapper2.select(null);
        for (User user : select) {
            System.out.println(user);
        }
    }
}

获得分⻚相关的其他参数

//其他分⻚的数据
PageInfo<User> pageInfo = new PageInfo<User>(select);
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());

七. 通⽤ mapper

(一)什么是通⽤Mapper

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

1. ⾸先在maven项⽬,在pom.xml中引⼊mapper的依赖

<dependency>
 <groupId>tk.mybatis</groupId>
 <artifactId>mapper</artifactId>
 <version>3.1.2</version>
</dependency>

2. Mybatis配置⽂件中完成配置

<plugins>
 <!--分⻚插件:如果有分⻚插件,要排在通⽤mapper之前-->
 <plugin interceptor="com.github.pagehelper.PageHelper">
 <property name="dialect" value="mysql"/>
 </plugin>
 <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
 <!-- 通⽤Mapper接⼝,多个通⽤接⼝⽤逗号隔开 -->
 <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
 </plugin>
</plugins>

3. 实体类设置主键

@Table(name = "t_user")
public class User {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Integer id;
 private String username; }

4. 定义通⽤mapper

import com.lagou.domain.User;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
}

5. 测试

public class UserTest {
    @Test
    public void test1() throws IOException {
        Inputstream resourceAsStream =
                Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory build = new
                SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = build.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        User user = new User();
        user.setId(4);
        //(1)mapper基础接⼝
        //select 接⼝
        User user1 = userMapper.selectOne(user); //根据实体中的属性进⾏查询,只能有 —个返回值
        List<User> users = userMapper.select(null); //查询全部结果
        userMapper.selectByPrimaryKey(1); //根据主键字段进⾏查询,⽅法参数必须包含完 整的主键属
        性,查询条件使⽤等号
        userMapper.selectCount(user); //根据实体中的属性查询总数,查询条件使⽤等号
        // insert 接⼝
        int insert = userMapper.insert(user); //保存⼀个实体,null值也会保存,不会使 ⽤数据库
        默认值
        int i = userMapper.insertSelective(user); //保存实体,null的属性不会保存, 会使⽤数据
        库默认值
        // update 接⼝
        int i1 = userMapper.updateByPrimaryKey(user);//根据主键更新实体全部字段, null值会被
        更新
        // delete 接⼝
        int delete = userMapper.delete(user); //根据实体属性作为条件进⾏删除,查询条件 使⽤等号
        userMapper.deleteByPrimaryKey(1); //根据主键字段进⾏删除,⽅法参数必须包含完 整的主键属
        性
        //(2)example⽅法
        Example example = new Example(User.class);
        example.createCriteria().andEqualTo("id", 1);
        example.createCriteria().andLike("val", "1");
        //⾃定义查询
        List<User> users1 = userMapper.selectByExample(example);
    }
}

节选自拉钩教育JAVA系列教程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值