首页 > 技术文章 > Mybatis 之 插件

yb-ken 2022-02-22 16:09 原文

插件编写(plugin) 

步骤:

1. 编写Interceptor的实现类:

MyFirstPlug

2. 使用@Intercepts注解完成插件签名

3. 将写好的插件注册到全局配置文件中

package com.feng.config;

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

import java.util.Properties;

/**
 * @Desc:完成插件签名:
 *        告诉mybatis当前插件用来拦截哪个对象的哪个方法
 * @Param: Sinature type:拦截哪个对象(四大对象)
 *                  method:对象的哪个方法
 *                  args: 当前方法的参数列表
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
})
public class MyFirstPlug implements Interceptor {

    /**
     * 拦截目标对象的目标方法的执行
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
//TODO ... 动态修改mybatis运行流程;比如sql分页、修改sql使用的参数。。。 //执行目标方法: parameterrize Object proceed = invocation.proceed(); //返回执行后的返回值
//TODO ... return proceed; }
/** * 包装目标对象,为目标对象创建一个代理对象 * @param target * @return */ @Override public Object plugin(Object target) { //借助Plugin的wrap方法来使用当前Interceptor包装目标对象 Object wrap = Plugin.wrap(target, this); //返回为当前target创建的动态代理对象 return wrap; } /** * 将插件注册时的property属性设置进来 * @param properties */ @Override public void setProperties(Properties properties) { System.out.println("插件配置信息:"+properties); String username = (String) properties.get("username"); String password = (String) properties.get("password"); } }

 

 mybatis-config.xml

<configuration>
   <!-- 配置全局插件 -->
    <plugins>
        <plugin interceptor="com.feng.config.MyFirstPlug">
            <property name="username" value="zhangsan"/>
            <property name="password" value="123456"/>
        </plugin>
    </plugins>
</configuration>

插件扩展:

插件拦截的场景:

场景一:修改sql使用的参数
package com.feng.config;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.util.Properties;

/**
 * @Desc:完成插件签名:
 *        告诉mybatis当前插件用来拦截哪个对象的哪个方法
 * @Param: Sinature type:拦截哪个对象(四大对象)
 *                  method:对象的哪个方法
 *                  args: 当前方法的参数列表
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
})
public class MyFirstPlug implements Interceptor {

    /**
     * 拦截目标对象的目标方法的执行
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("intercept:"+invocation.getTarget());
        //动态的改变一下sql的参数:
        Object target = invocation.getTarget();
        System.out.println("当前拦截的对象:"+target);
        //拿到:StatementHandler ==>ParameterHandler ==>parameterObject
        //拿到target的元数据
        MetaObject metaObject = SystemMetaObject.forObject(target);
        Object value = metaObject.getValue("parameterHandler.parameterObject");
        System.out.println("sql语句的参数:"+value);
        //修改sql语句要用的参数
        metaObject.setValue("parameterHandler.parameterObject", 12);
        //返回执行后的返回值
        Object proceed = invocation.proceed();
        return proceed;
    }

    /**
     * 包装目标对象,为目标对象创建一个代理对象
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        //借助Plugin的wrap方法来使用当前Interceptor包装目标对象
        Object wrap = Plugin.wrap(target, this);
        //返回为当前target创建的动态代理对象
        return wrap;
    }

    /**
     * 将插件注册时的property属性设置进来
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置信息:"+properties);
        String username = (String) properties.get("username");
        String password = (String) properties.get("password");
    }
}

 

场景二:PageHelper插件进行分页

a . 引入依赖

在pom.xml文件添加如下依赖:

<!-- mybatis分页插件依赖 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>4.2.0</version>
</dependency>

b. 在Mybatis配置xml中配置拦截器插件

在mybatis-config.xml文件中添加插件后的内容如下:

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">  
<configuration>    
<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageHelper">
        <property name="dialect" value="mysql"/>
        <!-- 该参数默认为false -->
        <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
        <!-- 和startPage中的pageNum效果一样-->
        <property name="offsetAsPageNum" value="true"/>
        <!-- 该参数默认为false -->
        <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
        <property name="rowBoundsWithCount" value="true"/>
        <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
        <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
        <property name="pageSizeZero" value="true"/>
        <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
        <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
        <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
        <property name="reasonable" value="false"/>
        <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
        <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
        <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 -->
        <!-- 不理解该含义的前提下,不要随便复制该配置 -->
        <property name="params" value="pageNum=start;pageSize=limit;"/>
        <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
        <property name="returnPageInfo" value="check"/>
    </plugin>
</plugins>
</configuration>

 c. 默认jar包中的实现类

package com.github.pagehelper;

import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * 最简单的分页插件拦截器
 */
@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PaginationInterceptor implements Interceptor {
    protected Dialect dialect;
    protected Field additionalParametersField;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取拦截方法的参数
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        List resultList;
        //调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!dialect.skip(ms, parameterObject, rowBounds)) {
            ResultHandler resultHandler = (ResultHandler) args[3];
            //当前的目标对象
            Executor executor = (Executor) invocation.getTarget();
            BoundSql boundSql = ms.getBoundSql(parameterObject);
            //反射获取动态参数
            Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
            //判断是否需要进行 count 查询
            if (dialect.beforeCount(ms, parameterObject, rowBounds)) {
                //创建 count 查询的缓存 key
                CacheKey countKey = executor.createCacheKey(ms, parameterObject, RowBounds.DEFAULT, boundSql);
                countKey.update("_Count");
                //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                MappedStatement countMs = MSUtils.newCountMappedStatement(ms);
                //调用方言获取 count sql
                String countSql = dialect.getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);
                BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
                //当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
                for (String key : additionalParameters.keySet()) {
                    countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                }
                //执行 count 查询
                Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
                Long count = (Long) ((List) countResultList).get(0);
                //处理查询总数
                dialect.afterCount(count, parameterObject, rowBounds);
                if (count == 0L) {
                    //当查询总数为 0 时,直接返回空的结果
                    return dialect.afterPage(new ArrayList(), parameterObject, rowBounds);
                }
            }
            //判断是否需要进行分页查询
            if (dialect.beforePage(ms, parameterObject, rowBounds)) {
                //生成分页的缓存 key
                CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
                //处理参数对象
                parameterObject = dialect.processParameterObject(ms, parameterObject, boundSql, pageKey);
                //调用方言获取分页 sql
                String pageSql = dialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
                BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
                //设置动态参数
                for (String key : additionalParameters.keySet()) {
                    pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                }
                //执行分页查询
                resultList = executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
            } else {
                resultList = new ArrayList();
            }
        } else {
            args[2] = RowBounds.DEFAULT;
            resultList = (List) invocation.proceed();
        }
        //返回默认查询
        return dialect.afterPage(resultList, parameterObject, rowBounds);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        String dialectClassStr = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClassStr)) {
            throw new RuntimeException("使用 PaginationInterceptor 分页插件时,必须设置 dialect 属性");
        }
        try {
            Class dialectClass = Class.forName(dialectClassStr);
            dialect = (Dialect) dialectClass.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("初始化 dialect [" + dialectClassStr + "]时出错:" + e.getMessage());
        }
        dialect.setProperties(properties);
        try {
            //反射获取 BoundSql 中的 additionalParameters 属性
            additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }
}

 d.使用page

 

 

 

 场景三:批量操作

与Spring整合进行批量操作

    <!--整合mybatis
        目的:1、Spring管理所有组件,
             2、Spring用来管理事务
    -->
    <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- configLocation 指定全局配置文件的位置-->
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
        <!--mapperLocations:指定mapper文件的位置-->
        <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"></property>
    </bean>
    <!-- 配置一个可以进行批量执行的sqlSession -->
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactoryBean"></constructor-arg>
        <constructor-arg name="executorType" ref="BATCH"></constructor-arg>
    </bean>

java中使用:
@Autowired
     private SqlSession sqlSession;

 

 

 场景四:存储过程

 

 场景五:typeHandler处理枚举

可以通过自定义TypeHandler的形式来在设置参数或取出结果集的时候自定义参数封装策略

步骤:

1. 实现TypeHandler接口或继承BaseTypeHandler

2. 使用@MappedTypes定义处理的java类型

3. 在自定义结果集标签或参数处理的时候声明使用自定义TypeHandler进行处理

    或在全局配置TypeHandler要处理的javaType

 

 

多插件编写:

<configuration>
   <!-- 配置全局插件 -->
    <plugins>
        <plugin interceptor="com.feng.config.MyFirstPlug">
            <property name="username" value="zhangsan"/>
            <property name="password" value="123456"/>
        </plugin>
        <plugin interceptor="com.feng.config.MySecondPlug"></plugin>
    </plugins>
</configuration>

 

 

 

推荐阅读