Mybatis插件:自定义插件分页及通用Mapper

Posted 丿涛哥哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis插件:自定义插件分页及通用Mapper相关的知识,希望对你有一定的参考价值。

Mybatis插件:自定义插件、分页及通用Mapper

1、 插件简介

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

2、 Mybatis插件介绍

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

MyBatis_42

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

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

3、 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.xml中。

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

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

以上就是MyBatis插件机制的基本原理。

4、 自定义插件

4.1、 插件接口

Mybatis 插件接口-Interceptor

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

4.2、 自定义插件

设计实现一个自定义插件:

Intercepts ({//注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这个拦截器
    @Signature (type = StatementHandler .class , //这是指拦截哪个接口
    method = "prepare"//这个接口内的哪个方法名,不要拼错了
    args = { Connection.class, Integer .class}),//这是拦截的方法的入参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
})
public class MyPlugin implements Interceptor {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    //这里是每次执行操作的时候,都会进行这个拦截器的方法内
    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 );
    }
}

sqlMapConfig.xml

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

mapper接口

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

mapper.xml

<mapper namespace="com.tao.mapper.UserMapper">
    <select id="selectUser" resultType="com.tao.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);
        }
    }
}

5、 源码分析

执行插件逻辑

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
    { //调用被拦截的方法
    >>

6、 pageHelper分页插件

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

开发步骤:

  1. 导入通用PageHelper的坐标
  2. 在mybatis核心配置文件中配置PageHelper插件
  3. 测试分页数据获取

1、导入通用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>

2、在mybatis核心配置文件中配置PageHelper插件

<!--注意:分页助手的插件 配置在通用馆mapper之前*-->
<plugin interceptor="com.github.pagehelper.PageHelper">
    <!—指定方言 >
    <property name="dialect" value="mysql"/>
</plugin>

3、测试分页代码实现

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

4、获得分页相关的其他参数

//其他分页的数据
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());

7、 通用 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.tao.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); //根据实体中的属性进行查询,只能有1个返回值
    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);
    }
}

以上是关于Mybatis插件:自定义插件分页及通用Mapper的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis 插件使用-自定义简单的分页插件

MyBatis-Plus03_分页插件自定义分页

MyBatis-Plus03_分页插件自定义分页

mybatis插件机制原理

浅谈使用PageHelper-Mybatis通用分页插件

Springboot集成mybatis通用Mapper与分页插件PageHelper(推荐)