mybatis中自定义插件的使用
介绍
基于mybatis框架强大的特性,mybatis允许我们在映射语句的执行过程中对某些方法的调用进行拦截加工,又因为其不是必须的,只根据需要创建,因此称之为自定义插件,实际上就是一个拦截器插件
。
那么既然是拦截器,一定都需要关注两个点:
- 拦截的对象是谁,即目标是谁?
- 拦截后要作何处理?
mybatis的执行流程中,有四个核心对象:
ParameterHandler
(getParameterObject、setParameters):参数处理器,处理SQL的参数对象ResultSetHandler
(handlerResultSets、handleOutputParameters等方法):结果集处理器,处理SQL的返回结果集StatementHandler
(prepare、parameterize、batch、update、query等方法):SQL语法构建器,数据库的处理对象,用于执行SQL语句Executor
(update、query、commit、rollback等方法):MyBatis的执行器,用于执行增删改查操作
那么mybatis拦截器主要针对的就是上面四个对象。
因为插件的作用对象是mybatis的四大核心对象,因此我们在使用插件的时候,一定非常谨慎,因为你操作的是mybatis最底层的类和方法。另外,在操作完成后,一定要记得执行拦截器的放行操作!
原理
mybatis插件所使用的到的核心的设计模式是动态代理模式
,利用JDK动态代理机制,为这些接口的实现类创建代理对象,在执行方法时,先去执行代理对象的方法,从而执行自己编写的拦截逻辑,所以真正要用好mybatis插件,主要还是要熟悉这四个接口的方法以及这些方法上的参数的含义。
- 设计模式:代理模式、责任链模式
- 软件思想:AOP编程思想,降低模块间的耦合度,使业务模块更加独立
在四大对象创建的时候:
- 每个创建出来的对象不是直接返回的,而是InterceptorChain.pluginAll(Object)
- 获取到所有的 Interceptor(拦截器),调用 Interceptor.plugin(target),返回包装后的对象
- 插件机制,我们可以使用插件为目标对象创建一个代理对象,AOP(面向切面)我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行
使用
mybatis的插件要想使用也很简单,只需要实现一个接口Interceptor(org.apache.ibatis.plugin包下的)
并重写方法即可。
另外需要用到这两个注解:
-
@Intercepts
注解标注该类为一个拦截器
-
@Signature
注解指明改拦截器需要拦截那个借口的哪一个方法
参数 描述 type
四种类型接口作品那个的某一个接口,如 Executor.class
method
对应接口中的某一个方法名,比如 Executor
的query
、update
等方法args
对应接口中的某一个方法的参数,比如 Executor
中query
方法因为重载原因,有多个,args
就是指明参数类型,从而确定是具体哪一个方法。
具体使用,官方给出的例子是:
// ExamplePlugin.java
//这里指明具体想要拦截处理哪些对象的哪些方法,是一个数组
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
//拦截后进行具体操作的方法,此方法必须重写
@Override
public Object intercept(Invocation invocation) throws Throwable {
// implement pre-processing if needed
Object returnObject = invocation.proceed();
// implement post-processing if needed
//操作完后一定要放行!
return returnObject;
}
//可以设置参数
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
同时也需要我们在配置文件中进行配置,当然,也可以通过全注解开发的方式实现。
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<!--传入配置参数-->
<property name="someProperty" value="100"/>
</plugin>
</plugins>
应用场景举例
mybatis中插件的应用还是比较广泛的,下面就几个使用场景简单举例。
这里我是在Spring中进行模拟的,如果是在Springboot中,则配置起来会更简单。
准备工作
数据库搭建
创建一个数据库,这里我命名为mybatis_example,并创建一个表book
表结构如下:
CREATE TABLE `book` (
`id` bigint unsigned NOT NULL,
`name` varchar(255) DEFAULT NULL,
`author` varchar(50) DEFAULT NULL,
`price` decimal(6,1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
测试环境搭建
- 创建一个maven工程,导入相关的jar包依赖:
<dependencies>
<!-- logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
<scope>test</scope>
</dependency>
<!--mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<!-- druid 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.9</version>
</dependency>
<!-- spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<!-- /spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.18</version>
</dependency>
<!-- spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.18</version>
</dependency>
<!-- mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
<!-- junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
-
配置spring以及mybatis,这里我使用的是全注解的方式,因此需要创建配置类:
SpringConfig
package com.soberw.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; /** * @author soberw */ @Configuration @Import(MybatisConfig.class) @ComponentScan("com.soberw") public class SpringConfig { }
MybatisConfig
package com.soberw.config; import com.alibaba.druid.pool.DruidDataSource; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.*; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import java.io.IOException; /** * @author soberw */ @Configuration //加载resources/db.properties 配置文件 @PropertySource("classpath:db.properties") //扫描有Mapper注解的接口 @Mapper 主要是解决,单元测试时报红 @ComponentScan("com.soberw.mapper") //也是扫描包,在接口上可以不加@Mapper @MapperScan("com.soberw.mapper") public class MybatisConfig { @Value("${db.driver:com.mysql.cj.jdbc.Driver}") private String driver; @Value("${db.url:jdbc:mysql:/mybatis_example}") private String url; @Value("${db.username:root}") private String username; @Value("${db.password:123456}") private String password; @Autowired private SnowFlakeInterceptor sfi; @Bean(name = "ds", initMethod = "init", destroyMethod = "close") @Primary public DruidDataSource ds() { DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUsername(username); ds.setPassword(password); ds.setUrl(url); return ds; } @Bean(name = "sf") @Primary public SqlSessionFactoryBean sf(DruidDataSource ds) throws IOException { SqlSessionFactoryBean sf = new SqlSessionFactoryBean(); sf.setDataSource(ds); // resources/mapper/XxxMapper.xml sf.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper*.xml")); //扫描定义别名 com.soberw.entity.Book 别名为 book sf.setTypeAliasesPackage("com.soberw.entity"); return sf; } }
-
创建实体类
Book
package com.soberw.entity; import lombok.Data; /** * @author soberw * @Classname Book * @Description * @Date 2022-04-12 20:51 */ @Data public class Book { /** * id */ private Long id; /** * 书名 */ private String name; /** * 作者 */ private String author; /** * 价格 */ private Double price; }
-
创建mapper映射类以及映射文件,添加插入和查询方法:
package com.soberw.mapper; import com.soberw.entity.Book; import org.apache.ibatis.annotations.*; import java.util.List; import java.util.Map; /** * @author soberw */ @Mapper public interface BookMapper { /** * 插入一条记录 * @param book * @return */ int insert(Book book); /** * 查询所有 * @return */ List<Book> selectAll(); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.soberw.mapper.BookMapper"> <insert id="insert" parameterType="book"> insert into book (id, name, author, price) values (#{id}, #{name}, #{author}, #{price}) </insert> <select id="selectAll" resultType="book"> select id,name,author,price from book </select> </mapper>
-
日志配置以及数据源配置
<?xml version="1.0"?> <configuration> <!-- ch.qos.logback.core.ConsoleAppender 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[%-5level] %d{HH:mm:ss} [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 日志级别 --> <root> <level value="off" /> <appender-ref ref="STDOUT" /> </root> <logger name="cn" level="DEBUG"/> </configuration>
db.driver=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/mybatis_example?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT db.username=root db.password=123456
设置主键插件
主键插件就是在添加表数据的时候,通过拦截器的形式自动为新数据添加主键,当前比较流行的主键设置是UUID或者雪花算法,这里以雪花算法为例,创建一个主键插件,在执行插入操作的时候自动为记录添加并设置主键。
因此需要一个用于生成雪花算法的类SnowflakeIdWorker
package com.soberw.entity;
/**
* Twitter_Snowflake
* SnowFlake的结构如下(每部分用-分开):
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
* 加起来刚好64位,为一个Long型。
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
* @author soberw
*/
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间截 (2019-09-27) */
private final long twepoch = 1569513600000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
}
}
生成的数不会重复且具有规律。
-
下面创建一个拦截器类
SnowFlakeInterceptor
,用于主键的设置:package com.soberw.interceptor; import com.soberw.entity.IdWorker; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.springframework.beans.factory.annotation.Autowired; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * @author soberw */ @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}) public class SnowFlakeInterceptor implements Interceptor { @Autowired private IdWorker idw; @Override public Object intercept(Invocation io) throws Throwable { MappedStatement ms = (MappedStatement) io.getArgs()[0]; if ("INSERT".equals(ms.getSqlCommandType().toString())) { Object obj = io.getArgs()[1]; if (obj instanceof HashMap) { Map<String, Object> map = (Map<String, Object>) obj; map.put("id", idw.nextId()); } else { Method m = obj.getClass().getMethod("setId", Long.class); m.invoke(obj, idw.nextId()); } return io.proceed(); }else{ return io.proceed(); } } /** * Plugin.wrap生成拦截代理对象 */ @Override public Object plugin(Object o) { if (o instanceof Executor) { return Plugin.wrap(o, this); } else { return o; } } @Override public void setProperties(Properties properties) { } }
-
接下来需要将插件注册到mybatis中去,因此需要在配置类中设置,如果你使用的是配置文件的方式,则在配置文件中配置即可:
首先使用@Bean将插件注入到spring容器中,然后设置到mybatis中:
-
接下来进行测试,创建一个test测试类:
package test; import com.soberw.config.SpringConfig; import com.soberw.entity.Book; import com.soberw.mapper.BookMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.util.List; /** * @author soberw * @Classname MyTest * @Description * @Date 2022-04-18 19:23 */ @SpringJUnitConfig(classes = SpringConfig.class) public class MyTest { @Autowired BookMapper bookMapper; @Test public void insert() { bookMapper.insert(new Book(null,"《javase》","张三",36.9)); bookMapper.insert(new Book(null,"《javaee》","李四",38.9)); bookMapper.insert(new Book(null,"《mysql》","王五",20.9)); bookMapper.insert(new Book(null,"《html》","马六",63.9)); bookMapper.insert(new Book(null,"《css》","赵七",96.3)); bookMapper.insert(new Book(null,"《JavaScript》","孙八",36.7)); List<Book> books = bookMapper.selectAll(); System.out.println("books = " + books); } }
可以看到,插入成功,查看数据:
分页插件
mybatis插件的另一个比较常用的使用场景就是分页处理了,分页操作是必不可少的一步操作,尤其是当数据量大的时候,而实际中实现分页的方式有很多种,可以直接通过编写SQL语句进行分页处理,也可以使用一些优秀的分页组件,当然,你完全也可以使用mybatis的插件技术进行分页处理。
实际上,已经存在一款分页插件了PageHelper
,我们只需要导入maven依赖,并通过简单的配置即可,官方网址点此
这里我们自己编写一个分页插件:
创建一个拦截器类MyPager
package com.soberw.interceptor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.Properties;
/**
* @author soberw
* @Classname MyPager
* @Description
* @Date 2022-04-18 19:46
*/
args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})})
public class MyPager implements Interceptor {
public static void startPage(int c, int p) {
currpage = c;
pagesize = p;
}
public static void startPage(int c) {
currpage = c;
}
public static int currpage = 1;
public static int pagesize = 10;
public static int recordcount = 0;
public static int pagecount = 1;
/**
* 插件的核心处理方法
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object sh = invocation.getTarget();
MetaObject mh = SystemMetaObject.forObject(sh);
MappedStatement ms = (MappedStatement) mh.getValue("delegate.mappedStatement");
String msid = ms.getId();
//判断方法是否以ByPage为后缀
if (msid.endsWith("ByPage")) {
mh.getValue("delegate.parameterHandler");
String sql =
//在原来的SQL语句上拼接上分页条件语句
mh.getValue("delegate.boundSql.sql").toString().trim() + String.format(" limit % d, % d ", currpage * pagesize - pagesize, pagesize);
mh.setValue("delegate.boundSql.sql", sql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
//固定的写法,第二个参数表示要代理的对象 this
return Plugin.wrap(target, this);
}
/**
* 要传给这个插件的配置信息
*
* @param p
*/
@Override
public void setProperties(Properties p) {
pagesize = Integer.parseInt(p.getOrDefault("pagesize", 15).toString());
currpage = Integer.parseInt(p.getOrDefault("currpage", 1).toString());
}
}
注入容器并设置到插件中:
测试:
若想查看第二页,则修改“currpage”即可。
当然 ,mybatis插件的使用场景远不止与此,比如:
- 很多场景中都会给表添加添加时间、更新时间字段,我们就可以通过插件自动设置字段值到数据库记录
- 对于一些用户比较敏感的信息,比如手机号、身份证号、密码等信息,可以通过插件在插入的时候加密处理,当需要查看时,再进行解密操作
- 还可以通过插件完成逻辑删除的操作
当然,mybatis插件的使用情景还有很多很多。