• 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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 {
        //省略逻辑
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    <plugins>
      <plugin interceptor="com.zjq.plugin.ExamplePlugin">plugin>
    plugins>
    
    • 1
    • 2
    • 3

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

    ⾃定义插件

    插件接口

    Mybatis 插件接⼝-Interceptor

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

    ⾃定义插件

    设计实现⼀个⾃定义插件

    @Intercepts({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤这个拦截器
        @Signature(type = StatementHandler.class , //这是指拦截哪个接⼝
        method = "prepare", //这个接⼝内的哪个⽅法名,不要拼错了
        args = { Connection.class, Integer .class}),  //这是拦截的⽅法的⼊参,按顺序写到这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
    })
    public class MyPlugin implements Interceptor {
        
        // //这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            //增强逻辑
            System.out.println("对⽅法进⾏了增强....");
            return invocation.proceed(); //执⾏原⽅法
        }
        /**
        * //主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中
        * ^Description包装⽬标对象 为⽬标对象创建代理对象
        * @Param target为要拦截的对象
        * @Return代理对象
        */
        @Override
        public Object plugin(Object target) {
            System.out.println("将要包装的⽬标对象:"+target);
            return Plugin.wrap(target,this);
        }
        /**获取配置⽂件的属性**/
        //插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
        @Override
        public void setProperties(Properties properties) {
            System.out.println("插件配置的初始化参数:"+properties );
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    sqlMapConfig.xml
    在这里插入图片描述

    mapper接⼝
    在这里插入图片描述

    mapper.xml

    <mapper namespace="com.zjq.mapper.UserMapper">
    
        
        <sql id="selectUser">
          select * from user
        sql>
    
        <select id="findByCondition" parameterType="user" resultType="user">
            <include refid="selectUser">include>
            <where>
                <if test="id!=0">
                    and id=#{id}
                if>
                <if test="username!=null and username!=''">
                    and username=#{username}
                if>
                <if test="password!=null and password!=''">
                    and password=#{password}
                if>
            where>
        select>
      mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    测试类

    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);
            User condition = new User();
            //condition.setId(1);
            condition.setUsername("zjq");
            List<User> byPaging = userMapper.findByCondition(condition);
            for (User user : byPaging) {
                System.out.println(user);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    源码分析

    执⾏插件逻辑
    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){
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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 { //调⽤被拦截的⽅法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    关于插件的执⾏逻辑就分析结束。

    pageHelper分页插件

    MyBatis可以使⽤第三⽅的插件来对功能进⾏扩展,分⻚助⼿PageHelper是将分⻚的复杂操作进⾏封装,使⽤简单的⽅式即可获得分⻚的相关数据
    开发步骤:

    1. 导⼊通⽤PageHelper坐标
    <dependency>
      <groupId>com.github.pagehelpergroupId>
      <artifactId>pagehelperartifactId>
      <version>3.7.5version>
    dependency>
    <dependency>
      <groupId>com.github.jsqlparsergroupId>
      <artifactId>jsqlparserartifactId>
      <version>0.9.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 在mybatis核⼼配置⽂件中配置PageHelper插件
    *
    <plugin interceptor="com.github.pagehelper.PageHelper">
      <!—指定⽅⾔ >
      <property name="dialect" value="mysql"/>
    plugin>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 测试分⻚代码实现
    @Test
    public void testPageHelper() {
        //设置分⻚参数
        PageHelper.startPage(1, 2);
        User condition = new User();
        //condition.setId(1);
        condition.setUsername("zjq");
        List<User> select = userMapper.findByCondition(condition);
        for (User user : select) {
            System.out.println(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    获得分⻚相关的其他参数

    //其他分⻚的数据
    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());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通⽤ mapper

    什么是通⽤Mapper

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

    如何使⽤

    1. ⾸先在maven项⽬,在pom.xml中引⼊mapper的依赖
    <dependency>
      <groupId>tk.mybatisgroupId>
      <artifactId>mapperartifactId>
      <version>3.1.2version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. Mybatis配置⽂件中完成配置
    <plugins>
      
      <plugin interceptor="com.github.pagehelper.PageHelper">
        <property name="dialect" value="mysql"/>
      plugin>
      <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
        
        <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
      plugin>
    plugins>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 实体类设置主键
    @Table(name = "t_user")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        private String username;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. 定义通⽤mapper
    import com.zjq.domain.User;
    import tk.mybatis.mapper.common.Mapper;
    public interface UserMapper extends Mapper<User> {
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 测试
    @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 接⼝
        //保存⼀个实体,null值也会保存,不会使⽤数据库默认值
        int insert = userMapper.insert(user);
        //保存实体,null的属性不会保存,会使⽤数据库默认值
        int i = userMapper.insertSelective(user); 
        
        // update 接⼝
        //根据主键更新实体全部字段,null值会被更新
        int i1 = userMapper.updateByPrimaryKey(user);
        
        // 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    本文内容到此结束了,
    如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
    如有错误❌疑问💬欢迎各位指出。
    主页共饮一杯无的博客汇总👨‍💻

    保持热爱,奔赴下一场山海。🏃🏃🏃

  • 相关阅读:
    java 处理树形结构数据
    rpc网络
    springboot使用mybatis
    计算机网络
    Ansible学习笔记15
    前端如何处理后端一次性传来的10w条数据
    Java根据ip地址获取归属地
    颜值在线的 Keychron K8 实际上手体验
    重新认识下JVM级别的本地缓存框架Guava Cache——优秀从何而来
    Git 远程仓库(Github)
  • 原文地址:https://blog.csdn.net/qq_35427589/article/details/131890996