springboot整合mybatis框架,手把手带你创建自定义mybatis插件,完成插入时使用自定义规则的id(雪花算法,时间戳随机数自增)

前言:

首先学习此博文前需要对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一次会话中的所有操作。

  1. Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
  2. StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。
  3. ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。
  4. 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);
}

这个接口只声明了三个方法。

  1. setProperties方法是在Mybatis进行配置插件的时候可以配置自定义相关属性,即:接口实现对象的参数配置
  2. plugin方法是插件用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理,可以决定是否要进行拦截进而决定要返回一个什么样的目标对象,官方提供了示例:return Plugin.wrap(target, this);
  3. 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值