文章目录
前言:
首先学习此博文前需要对mybatis有系统的学习,尤其是了解mybatis的插件plugin运行步骤原理等,同时需要已经基本掌握springboot框架技术,不然可能手忙脚乱,整合起来非常费劲。springboot已经到2.4了,更得很快,但是2.4.0版本的mybatis-spring-boot-starter, mybatis官方还没适配,所以使用现在适配的2.3.6。
附上相对应官网文档地址:
springboot:https://docs.spring.io/spring-boot/docs/2.3.6.RELEASE/reference/html/
mybatis:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
同时先了解整合的案例种需要用到哪些包:MySQL,Spring Web,JDBC,MyBatis,以及springboot
1.整合mybatis
可参考此前的博客,其实很简单就是步骤就是创spring boot的项目,然后选择相关的依赖数据库连接驱动啊,mybatis-spring-boot-starter等,同时注意包路径和主类的位置,同时在主类开启@MapperScan指明包路径或是在@Configuration的类上使用即可,然后只要在集成的单元测试环境简单测试一下即可!
我之前有一篇有关整合以及逆向工程的博客,可供参考:
https://blog.csdn.net/xtho62/article/details/109374447
2.整合使用mybatis plugin,实现自定义id插入
整合的关键还是要回到mybatis的运行原理得出,先看下图
可见在Executor到传递数据库之前都是有一系列的处理器在处理,Mybatis插件又称拦截器,本篇文章中出现的拦截器都表示插件。
Mybatis采用责任链模式,通过动态代理组织多个插件(拦截器),通过这些插件可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出安全高效的插件。
而官方文档也给出模板使用以及对于拦截哪些方法
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
总体概括为:
- 拦截执行器的方法
- 拦截参数的处理
- 拦截结果集的处理
- 拦截Sql语法构建的处理
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
官方原话引用,使用的时候要当心,不要胡乱修改,其实就按套路出牌即可。
Mybatis是通过动态代理的方式实现拦截的,阅读此篇文章需要先对Java的动态代理机制有所了解。可以参考博客:https://www.cnblogs.com/flyoung2008/archive/2013/08/11/3251148.html
Mybatis插件能够对则四大对象进行拦截,可以包含到了Mybatis一次会话中的所有操作。
- Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
- StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。
- ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。
- ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改。
1.看看mybatis源码
1.plugin相关
Mybatis的插件实现要实现Interceptor接口,我们看下这个接口定义的方法。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
这个接口只声明了三个方法。
- setProperties方法是在Mybatis进行配置插件的时候可以配置自定义相关属性,即:接口实现对象的参数配置
- plugin方法是插件用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方提供了示例:return Plugin.wrap(target, this);
- intercept方法就是要进行拦截的时候要执行的方法
理解这个接口的定义,先要知道java动态代理机制
。plugin接口即返回参数target对象(Executor/ParameterHandler/ResultSetHander/StatementHandler)的代理对象。在调用对应对象的接口的时候,可以进行拦截并处理。
2.Mybatis四大接口对象创建方法
Mybatis的插件是采用对四大接口的对象生成动态代理对象的方法来实现的。那么现在我们看下Mybatis是怎么创建这四大接口对象的。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//确保ExecutorType不为空(defaultExecutorType有可能为空)
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor; if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
} if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
查看源码可以发现, Mybatis框架在创建好这四大接口对象的实例后,都会调用InterceptorChain.pluginAll()方法。InterceptorChain对象是插件执行链对象,看源码就知道里面维护了Mybatis配置的所有插件(Interceptor)对象。
// target --> Executor/ParameterHandler/ResultSetHander/StatementHandler
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
其实就是安顺序执行我们插件的plugin方法,一层一层返回我们原对象(Executor/ParameterHandler/ResultSetHander/StatementHandler)的代理对象。当我们调用四大接口对象的方法时候,实际上是调用代理对象的响应方法,代理对象又会调用四大接口对象的实例。
3.Plugin对象
我们知道,官方推荐插件实现plugin方法为:Plugin.wrap(target, this);
public static Object wrap(Object target, Interceptor interceptor) {
// 获取插件的Intercepts注解
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
}
return target;
}
这个方法其实是Mybatis简化我们插件实现的工具方法。其实就是根据当前拦截的对象创建了一个动态代理对象。代理对象的InvocationHandler处理器为新建的Plugin对象。
4.插件配置注解@Intercepts
Mybatis的插件都要有Intercepts注解来指定要拦截哪个对象的哪个方法。我们知道,Plugin.warp方法会返回四大接口对象的代理对象(通过new Plugin()创建的IvocationHandler处理器),会拦截所有的执行方法。在代理对象执行对应方法的时候,会调用InvocationHandler处理器的invoke方法。Mybatis中利用了注解的方式配置指定拦截哪些方法。具体如下:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
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) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
可以看到,只有通过Intercepts注解指定的方法才会执行我们自定义插件的intercept方法。未通过Intercepts注解指定的将不会执行我们的intercept方法。
5.官方插件开发方式
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class TestInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget(); //被代理对象
Method method = invocation.getMethod(); //代理方法
Object[] args = invocation.getArgs(); //方法参数
// do something ...... 方法拦截前执行代码块
Object result = invocation.proceed();
// do something .......方法拦截后执行代码块
return result;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
2.接下来开始我们的案例
环境准备
此前在git看到一位某大佬的代码,本文的代码也整合也基本上是参考该大佬代码,然后自己动手结合一些已有知识进行尝试!!现在已找不到出处。很可惜,暂时使用原创,如大佬请看见私信我进行调整。
<!--谷歌工具类-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.10</version>
</dependency>
好,然后说明简单说明一下本案例,本案例是通过实现mybatis的拦截器作为一个插件,在执行插入方法的时候,生成我们自定义规则的id。本文使用一张用户表进行演示,而实体类则是两个,一个是和表字段完全对应的实体类,一个是视图实体类,因为在很多场景中,插入的时候并不会一下子然后用户填完所有的信息,尤其是注册的时候,一般是注册之后再完善,这样用户体验才不会太差。还有数据源方面使用的则是spring boot2默认是使用的hikari(号称世界最快数据源),也说说为什么使用这个,因为我看网上大多数是使用druid写java配置类,而hikari则都是使用配置文件配置,所以就决定使用hikari来做本案例演示啦。
创建数据库和表都使用utf8,注意老生常谈了mysql的utf8使用的是带mb4的。我演示创建的库名是myplugin,ok上建表sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL COMMENT 'id',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '姓名',
`sex` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '性别',
`age` int(255) NULL DEFAULT NULL COMMENT '年龄',
`create_time` datetime(0) NOT NULL COMMENT '创建时间',
`update_time` datetime(0) NOT NULL ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`status` int(10) NOT NULL DEFAULT 0 COMMENT '是否删除 1删除 0未删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
然后先看看我们最后的项目结构:
再com.tho下创建实体类包一个entity,一个vo:
package com.tho.entity;
import com.tho.plugin.AutoId;
import java.util.Date;
/**
* user表
*/
public class TabUser {
/**
* id(添加自定义主键ID)
*/
@AutoId
private Long id;
/**
* 姓名
*/
private String name;
/**
* 性别
*/
private String sex;
/**
* 年龄
*/
private Integer age;
/**
*
*/
private Date createTime;
/**
*
*/
private Date updateTime;
/**
* 是否删除 1删除 0未删除
*/
private Integer status;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public TabUser(Long id, String name, String sex, Integer age, Date createTime, Date updateTime, Integer status) {
this.id = id;
this.name = name;
this.sex = sex;
this.age = age;
this.createTime = createTime;
this.updateTime = updateTime;
this.status = status;
}
public TabUser(){}
@Override
public String toString() {
return "TabUser{" +
"id=" + id +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
", createTime=" + createTime +
", updateTime=" + updateTime +
", status=" + status +
'}';
}
}
package com.tho.vo;
/**
* 用户VO
*/
public class UserVO {
/**
* 姓名
*/
private String name;
/**
* 性别
*/
private String sex;
/**
* 年龄
*/
private Integer age;
public UserVO(){}
public UserVO(String name, String sex, Integer age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
实现功能代码
首先先配置插件以及封装注解,注意这个是mybatis的一个要求,作为mybaits的自定义插件就必须封装注解的。上代码:
package com.tho.plugin;
import java.lang.annotation.*;
/**
* @Description: 主键注解
* 支持三种主键:雪花ID (Long和String)和 UUID(String)和时间戳(Long)
* @author calmtho
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AutoId {
/**
* @return id类型(默认为雪花id)
*/
IdType value() default IdType.SNOWFLAKE;
/**
* id类型
*/
enum IdType {
/**
* UUID去掉“-”
*/
UUID,
/**
* 雪花id
*/
SNOWFLAKE,
/**
* 时间戳加随机数
*/
TimestampRan
}
}
package com.tho.plugin;
import java.text.SimpleDateFormat;
import static java.lang.Long.parseLong;
/**
* 获取18位随机数
* 4位年份+11位时间戳(毫秒值截取2位)+3位随机数
* @author
*/
public class GuidUtils {
/**
* 时间戳后的末尾的数字id
*/
private static volatile int Guid = 100;
private static class TimestampRan {
public static String getGuid() {
GuidUtils.Guid += 1;
long now = System.currentTimeMillis();
//获取4位年份数字
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy");
//获取时间戳
String time = dateFormat.format(now);
String info = now + "";
//获取三位随机数
//int ran=(int) ((Math.random()*9+1)*100);
//要是一段时间内的数据连过大会有重复的情况,所以做以下修改
int ran = 0;
if (GuidUtils.Guid > 999) {
GuidUtils.Guid = 100;
}
ran = GuidUtils.Guid;
return time + info.substring(2, info.length()) + ran;
}
}
/**
* 获取long类型时间戳随机数ID
*/
public static long TimestampRanLong() {
return parseLong(TimestampRan.getGuid());
}
}
package com.tho.plugin;
/**
* @author xub
* @Description: 雪花算法
* @date 2019/8/14 下午8:22
*/
public class SnowIdUtils {
/**
* 私有的 内部静态类
*/
private static class SnowFlake {
/**
* 内部类对象
*/
private static final SnowFlake SNOW_FLAKE = new SnowFlake();
/**
* 起始的时间戳
*/
private final long START_TIMESTAMP = 1557489395327L;
/**
* 序列号占用位数
*/
private final long SEQUENCE_BIT = 12;
/**
* 机器标识占用位数
*/
private final long MACHINE_BIT = 10;
/**
* 时间戳位移位数
*/
private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;
/**
* 最大序列号 (4095)
*/
private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
/**
* 最大机器编号 (1023)
*/
private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
/**
* 生成id机器标识部分
*/
private long machineIdPart;
/**
* 序列号
*/
private long sequence = 0L;
/**
* 上一次时间戳
*/
private long lastStamp = -1L;
/**
* 构造函数初始化机器编码
*/
private SnowFlake() {
//模拟这里获得本机机器编码
long localIp = 4321;
//localIp & MAX_MACHINE_ID最大不会超过1023,在左位移12位
machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT;
}
/**
* 获取雪花ID
*/
public synchronized long nextId() {
long currentStamp = timeGen();
//避免机器时钟回拨
while (currentStamp < lastStamp) {
// //服务器时钟被调整了,ID生成器停止服务.
throw new RuntimeException(String.format("时钟已经回拨. Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
}
if (currentStamp == lastStamp) {
// 每次+1
sequence = (sequence + 1) & MAX_SEQUENCE;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
currentStamp = getNextMill();
}
} else {
//不同毫秒内,序列号置0
sequence = 0L;
}
lastStamp = currentStamp;
//时间戳部分+机器标识部分+序列号部分
return (currentStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*/
private long getNextMill() {
long mill = timeGen();
//
while (mill <= lastStamp) {
mill = timeGen();
}
return mill;
}
/**
* 返回以毫秒为单位的当前时间
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
/**
* 获取long类型雪花ID
*/
public static long uniqueLong() {
return SnowFlake.SNOW_FLAKE.nextId();
}
/**
* 获取String类型雪花ID
*/
public static String uniqueLongHex() {
return String.format("%016x", uniqueLong());
}
}
package com.tho.plugin;
import com.google.common.base.Predicate;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.reflections.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* mybatis自定义拦截器插件
*/
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class
}),
})
public class AutoIdInterceptor implements Interceptor {
/**
* key值为class对象 value可以理解成是该类带有AutoId注解的属性,只不过对属性封装了一层。
* 它是非常能够提高性能的处理器 它的作用就是不用每一次一个对象经来都要看下它的哪些属性带有AutoId注解
* 毕竟类的反射在性能上并不友好。只要key包含该对象那就不需要检查它哪些属性带AutoId注解。
*/
private Map<Class, List<Handler>> handlerMap = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
//args数组对应对象就是上面@Signature注解中args对应的对应类型
MappedStatement mappedStatement = (MappedStatement) args[0];
//实体对象
Object entity = args[1];
if ("INSERT".equalsIgnoreCase(mappedStatement.getSqlCommandType().name())) {
// 获取实体集合
Set<Object> entitySet = getEntitySet(entity);
// 批量设置id
for (Object object : entitySet) {
process(object);
}
}
return invocation.proceed();
}
/**
* object是需要插入的实体数据,它可能是对象,也可能是批量插入的对象。
* 如果是单个对象,那么object就是当前对象
* 如果是批量插入对象,那么object就是一个map集合,key值为"list",value为ArrayList集合对象
*/
private Set<Object> getEntitySet(Object object) {
Set<Object> set = new HashSet<>();
if (object instanceof Map) {
//批量插入对象
Collection values = (Collection) ((Map) object).get("list");
System.out.println("values = " + values);
for (Object value : values) {
if (value instanceof Collection) {
set.addAll((Collection) value);
} else {
set.add(value);
}
}
} else {
//单个插入对象
set.add(object);
}
return set;
}
private void process(Object object) throws Throwable {
Class handlerKey = object.getClass();
List<Handler> handlerList = handlerMap.get(handlerKey);
//TODO 性能优化点,如果有两个都是user对象同时,那么只需有个进行反射处理属性就好了,另一个只需执行下面的for循环
SYNC:
if (handlerList == null) {
synchronized (this) {
handlerList = handlerMap.get(handlerKey);
//如果到这里map集合已经存在,则跳出到指定SYNC标签
if (handlerList != null) {
break SYNC;
}
handlerMap.put(handlerKey, handlerList = new ArrayList<>());
// 反射工具类 获取带有AutoId注解的所有属性字段
Set<Field> allFields = ReflectionUtils.getAllFields(
object.getClass(),
(Predicate<Field>) input -> input != null && input.getAnnotation(AutoId.class) != null
);
for (Field field : allFields) {
AutoId annotation = field.getAnnotation(AutoId.class);
//1、添加UUID字符串作为主键
if (field.getType().isAssignableFrom(String.class)) {
if (annotation.value().equals(AutoId.IdType.UUID)) {
handlerList.add(new UUIDHandler(field));
//2、添加String类型雪花ID
} else if (annotation.value().equals(AutoId.IdType.SNOWFLAKE)) {
handlerList.add(new UniqueLongHexHandler(field));
}
} else if (field.getType().isAssignableFrom(Long.class)) {
//3、添加Long类型的雪花ID
if (annotation.value().equals(AutoId.IdType.SNOWFLAKE)) {
handlerList.add(new UniqueLongHandler(field));
}
else if (annotation.value().equals(AutoId.IdType.TimestampRan)){
//4.添加Long类型的时间戳加随机数
handlerList.add(new TimestampRanHandler(field));
}
}
}
}
}
for (Handler handler : handlerList) {
handler.accept(object);
}
}
private static abstract class Handler {
Field field;
Handler(Field field) {
this.field = field;
}
abstract void handle(Field field, Object object) throws Throwable;
private boolean checkField(Object object, Field field) throws IllegalAccessException {
if (!field.isAccessible()) {
field.setAccessible(true);
}
//如果该注解对应的属性已经被赋值,那么就不用通过雪花生成的ID
return field.get(object) == null;
}
public void accept(Object o) throws Throwable {
if (checkField(o, field)) {
handle(field, o);
}
}
}
private static class UUIDHandler extends Handler {
UUIDHandler(Field field) {
super(field);
}
/**
* 1、插入UUID主键
*/
@Override
void handle(Field field, Object object) throws Throwable {
field.set(object, UUID.randomUUID().toString().replace("-", ""));
}
}
private static class UniqueLongHandler extends Handler {
UniqueLongHandler(Field field) {
super(field);
}
/**
* 2、插入Long类型雪花ID
*/
@Override
void handle(Field field, Object object) throws Throwable {
field.set(object, SnowIdUtils.uniqueLong());
}
}
private static class UniqueLongHexHandler extends Handler {
UniqueLongHexHandler(Field field) {
super(field);
}
/**
* 3、插入String类型雪花ID
*/
@Override
void handle(Field field, Object object) throws Throwable {
field.set(object, SnowIdUtils.uniqueLongHex());
}
}
private static class TimestampRanHandler extends Handler{
TimestampRanHandler(Field field){super(field);}
/**
* 4.插入Long类型,时间戳加随机数趋势递增
*/
@Override
void handle(Field field, Object object) throws Throwable {
field.set(object,GuidUtils.TimestampRanLong());
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
先编写自定义注解,加上我们的生成id方法,这个自定义id在我以前的博客里面已经有过演示,可回看: https://blog.csdn.net/xtho62/article/details/108251058
接下来是配置数据源:
package com.tho.config;
import com.tho.plugin.AutoIdInterceptor;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@ComponentScan(basePackageClasses =DataSourceConfig.class)
@MapperScan(basePackages = "com.tho.mapper")
public class DataSourceConfig {
/**
* 使用java配置类配置hikariDataSource数据源信息
* */
@Bean
public DataSource getDataSource(){
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/myplugin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8");
hikariDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
hikariDataSource.setUsername("root");
hikariDataSource.setPassword("root3306");
hikariDataSource.setConnectionInitSql("SET NAMES utf8mb4");
hikariDataSource.setMaximumPoolSize(20);
return hikariDataSource;
}
/**
* 事务管理
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(getDataSource());
}
/**
* 插件实体
*/
@Bean
AutoIdInterceptor autoIdInterceptor() {
return new AutoIdInterceptor();
}
/**
* SqlSessionFactory 实体
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(getDataSource());
sessionFactory.setFailFast(true);
sessionFactory.setMapperLocations(resolver.getResources("classpath:/mapper/*Mapper.xml"));
/**
* 添加插件信息(因为插件采用责任链模式所有可以有多个,所以采用数组
*/
Interceptor[] interceptors = new Interceptor[1];
interceptors[0] = autoIdInterceptor();
sessionFactory.setPlugins(interceptors);
return sessionFactory.getObject();
}
}
重点是sessionFactory的配置!!
紧接着就是开始编写ssm三层啦!
mapper层:
我们在java配置类中已经声明映射位置啦,别放错
package com.tho.mapper;
import com.tho.entity.TabUser;
import java.util.List;
/**
* UserMapper接口
*/
public interface UserMapper {
/**
* 插入一条记录
*
* @param record 实体对象
* @return 更新条目数
*/
int insert(TabUser record);
/**
* 动态插入一条记录
*
* @param record 实体对象
* @return 更新条目数
*/
int insertSelective(TabUser record);
/**
* 根据主键查询
*
* @param id 主键
* @return 实体对象
*/
TabUser selectByPrimaryKey(Long id);
/**
* 批量插入
*
* @param list 插入集合
* @return 插入数量
*/
int insertForeach(List<TabUser> list);
/**
* 根据主键动态更新记录
*
* @param record 实体对象
* @return 更新条目数
*/
int updateByPrimaryKeySelective(TabUser record);
}
UserMapper.xml
<?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.tho.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.tho.entity.TabUser">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="sex" jdbcType="VARCHAR" property="sex" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="status" jdbcType="INTEGER" property="status" />
</resultMap>
<sql id="Base_Column_List">
id, name, sex, age, create_time, update_time, status
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from user
where id = #{id,jdbcType=INTEGER}
</select>
<insert id="insert" parameterType="com.tho.entity.TabUser">
insert into user (id, name, sex,
age, create_time, update_time,
status)
values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{sex,jdbcType=VARCHAR},
#{age,jdbcType=INTEGER}, #{createTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP},
#{status,jdbcType=INTEGER})
</insert>
<insert id="insertForeach" parameterType="java.util.List" useGeneratedKeys="false">
insert into user (id, name, sex,
age, create_time, update_time,
status)
values
<foreach collection="list" item="item" index="index" separator=",">
(#{item.id,jdbcType=BIGINT}, #{item.name,jdbcType=VARCHAR}, #{item.sex,jdbcType=VARCHAR},
#{item.age,jdbcType=INTEGER}, #{item.createTime,jdbcType=TIMESTAMP}, #{item.updateTime,jdbcType=TIMESTAMP},
#{item.status,jdbcType=INTEGER})
</foreach>
</insert>
<insert id="insertSelective" parameterType="com.tho.entity.TabUser">
insert into user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="name != null">
name,
</if>
<if test="sex != null">
sex,
</if>
<if test="age != null">
age,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="updateTime != null">
update_time,
</if>
<if test="status != null">
status,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=BIGINT},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
<if test="sex != null">
#{sex,jdbcType=VARCHAR},
</if>
<if test="age != null">
#{age,jdbcType=INTEGER},
</if>
<if test="createTime != null">
#{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updateTime != null">
#{updateTime,jdbcType=TIMESTAMP},
</if>
<if test="status != null">
#{status,jdbcType=INTEGER},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.tho.entity.TabUser">
update user
<set>
<if test="name != null">
name = #{name,jdbcType=VARCHAR},
</if>
<if test="sex != null">
sex = #{sex,jdbcType=VARCHAR},
</if>
<if test="age != null">
age = #{age,jdbcType=INTEGER},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=TIMESTAMP},
</if>
<if test="status != null">
status = #{status,jdbcType=INTEGER},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>
service层:
package com.tho.service;
import com.tho.vo.UserVO;
import java.util.List;
/**
* @Description: 用户相关接口
*/
public interface UserService {
/**
* 批量 保存用户信息
* @param userVOList
*/
String insertForeach(List<UserVO> userVOList);
/**
* 单个 保存用户信息
* @param userVO
*/
String saveOne(UserVO userVO);
}
package com.tho.service.impl;
import com.google.common.collect.Lists;
import com.tho.entity.TabUser;
import com.tho.mapper.UserMapper;
import com.tho.service.UserService;
import com.tho.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* @Description: 用户实现类
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public String insertForeach(List<UserVO> userVOList) {
//实体转换
List<TabUser> tabUserList = Lists.newArrayListWithCapacity(userVOList.size());
for (UserVO userVO : userVOList) {
TabUser tabUser = build(userVO);
tabUserList.add(tabUser);
}
//批量插入数据
userMapper.insertForeach(tabUserList);
return "保存成功";
}
@Override
public String saveOne(UserVO userVO) {
TabUser tabUser = build(userVO);
userMapper.insert(tabUser);
return "保存成功";
}
/**
* 实体转换
*/
private TabUser build(UserVO vo) {
TabUser tabUser = new TabUser();
tabUser.setName(vo.getName());
tabUser.setSex(vo.getSex());
tabUser.setAge(vo.getAge());
tabUser.setCreateTime(new Date());
tabUser.setUpdateTime(new Date());
tabUser.setStatus(1);
return tabUser;
}
}
这层有两个注意的点:一个service的实现类必须加@Service注解
,接口可以不加,不然注入会报错,第二个是在实现层进行了对象的转换
,这里是关键,注意别写错
Controller层:
package com.tho.controller;
import com.google.common.collect.Lists;
import com.tho.service.UserService;
import com.tho.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* @Description: 接口测试
*/
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* 模拟插入数据
*/
List<UserVO> userVOList = Lists.newArrayList();
/**
* 初始化插入数据
*/
@PostConstruct
private void getData() {
userVOList.add(new UserVO("小丽", "女", 3));
userVOList.add(new UserVO("张三", "男", 33));
userVOList.add(new UserVO("李四", "女", 33));
userVOList.add(new UserVO("小六", "男", 66));
userVOList.add(new UserVO("王姐", "女", 66));
}
/**
* @Description: 批量保存用户接口
*/
@PostMapping("save-foreach-user")
public Object save() {
return userService.insertForeach(userVOList);
}
/**
* @Description: 单个保存用户接口
*/
@PostMapping("save-one-user")
public Object saveOne() {
return userService.saveOne(new UserVO("calmtho", "男", 99));
}
}
前面讲的直接模拟post请求构造!!
单元测试,测一下:
package com.tho;
import com.tho.controller.UserController;
import com.tho.entity.TabUser;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MypluginApplicationTests {
@Autowired
private UserController userController;
private final Logger log = LoggerFactory.getLogger(TabUser.class);
@Test
void contextLoads() {
userController.saveOne();
}
}
搭脚手架的时候就有生成:
package com.tho;
import com.tho.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MypluginApplicationTests {
@Autowired
private UserController userController;
//private final Logger log = LoggerFactory.getLogger(TabUser.class);
@Test
void contextLoads() {
userController.saveOne();
}
@Test
public void testBatch(){
userController.save();
}
}
先测单个插入:
单个插入,id为:202005363980368101
测试批量插入:
全都是2020开头然后趋势递增,和我们想要的效果一致:
换一下类雪花算法试试:(我们设置了默认就是雪花,为了演示下面还是指明吧)
单个插入生成的id为:200802122345615360
至此你已经跟着完成了编写mybatis插件,插入自定义规则的id了。
代码仓库:https://gitee.com/calmtho/springboot