springboot2结合mybatis拦截器实现主键自动生成

Easter79
• 阅读 947

前言

前阵子和朋友聊天,他说他们项目有个需求,要实现主键自动生成,不想每次新增的时候,都手动设置主键。于是我就问他,那你们数据库表设置主键自动递增不就得了。他的回答是他们项目目前的id都是采用雪花算法来生成,因此为了项目稳定性,不会切换id的生成方式。

朋友问我有没有什么实现思路,他们公司的orm框架是mybatis,我就建议他说,不然让你老大把mybatis切换成mybatis-plus。mybatis-plus就支持注解式的id自动生成,而且mybatis-plus只是对mybatis进行增强不做改变。朋友还是那句话,说为了项目稳定,之前项目组没有使用mybatis-plus的经验,贸然切换不知道会不会有什么坑。后面没招了,我就跟他说不然你用mybatis的拦截器实现一个吧。于是又有一篇吹水的创作题材出现。

前置知识

在介绍如何通过mybatis拦截器实现主键自动生成之前,我们先来梳理一些知识点

1、mybatis拦截器的作用

mybatis拦截器设计的初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动mybatis固有的逻辑

2、Interceptor拦截器

每个自定义拦截器都要实现

org.apache.ibatis.plugin.Interceptor

这个接口,并且自定义拦截器类上添加@Intercepts注解

3、拦截器能拦截哪些类型

  • Executor:拦截执行器的方法。

  • ParameterHandler:拦截参数的处理。

  • ResultHandler:拦截结果集的处理。

  • StatementHandler:拦截Sql语法构建的处理。

4、拦截的顺序

a、不同类型拦截器的执行顺序

Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

b、多个拦截器拦截同种类型同一个目标方法,执行顺序是后配置的拦截器先执行

比如在mybatis配置如下

    <plugins>
        <plugin interceptor="com.lybgeek.InterceptorA" />
        <plugin interceptor="com.lybgeek.InterceptorB" />
    </plugins>

则InterceptorB先执行。

如果是和spring做了集成,先注入spring ioc容器的拦截器,则后执行。比如有个mybatisConfig,里面有如下拦截器bean配置

 @Bean
    public InterceptorA interceptorA(){
        return new InterceptorA();
    }

    @Bean
    public InterceptorB interceptorB(){
        return new InterceptorB();
    }

则InterceptorB先执行。当然如果你是直接用@Component注解这形式,则可以配合@Order注解来控制加载顺序

5、拦截器注解介绍

@Intercepts:标识该类是一个拦截器

@Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法。 @Signature注解属性中的type表示对应可以拦截四种类型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的一种;method表示对应类型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的哪类方法;args表示对应method中的参数类型

6、拦截器方法介绍

a、 intercept方法

public Object intercept(Invocation invocation) throws Throwable

这个方法就是我们来执行我们自己想实现的业务逻辑,比如我们的主键自动生成逻辑就是在这边实现。

Invocation这个类中的成员属性target就是@Signature中的type;method就是@Signature中的method;args就是@Signature中的args参数类型的具体实例对象

b、 plugin方法

public Object plugin(Object target)

这个是用返回代理对象或者是原生代理对象,如果你要返回代理对象,则返回值可以设置为

Plugin.wrap(target, this);
this为拦截器

如果返回是代理对象,则会执行拦截器的业务逻辑,如果直接返回target,就是没有拦截器的业务逻辑。说白了就是告诉mybatis是不是要进行拦截,如果要拦截,就生成代理对象,不拦截是生成原生对象

c、 setProperties方法

public void setProperties(Properties properties)

用于在Mybatis配置文件中指定一些属性

主键自动生成思路

1、定义一个拦截器

主要拦截

 `Executor#update(MappedStatement ms, Object parameter)`} 

这个方法。mybatis的insert、update、delete都是通过这个方法,因此我们通过拦截这个这方法,来实现主键自动生成。其代码块如下

@Intercepts(value={@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class AutoIdInterceptor implements Interceptor {}

2、判断sql操作类型

Executor 提供的方法中,update 包含了 新增,修改和删除类型,无法直接区分,需要借助 MappedStatement 类的属性 SqlCommandType 来进行判断,该类包含了所有的操作类型

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

当SqlCommandType类型是insert我们才进行主键自增操作

3、填充主键值

3.1、编写自动生成id注解

Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
    /**
     * 主键名
     * @return
     */
    String primaryKey();

    /**
     * 支持的主键算法类型
     * @return
     */
    IdType type() default IdType.SNOWFLAKE;

    enum IdType{
        SNOWFLAKE
    }
}

3.2、 雪花算法实现

我们可以直接拿hutool这个工具包提供的idUtil来直接实现算法。

引入

 <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>


Snowflake snowflake = IdUtil.createSnowflake(0,0);
long value = snowflake.nextId();

3.3、填充主键值

其实现核心是利用反射。其核心代码片段如下

   ReflectionUtils.doWithFields(entity.getClass(), field->{
                    ReflectionUtils.makeAccessible(field);
                    AutoId autoId = field.getAnnotation(AutoId.class);
                    if(!ObjectUtils.isEmpty(autoId) && (field.getType().isAssignableFrom(Long.class))){
                        switch (autoId.type()){
                            case SNOWFLAKE:
                                SnowFlakeAutoIdProcess snowFlakeAutoIdProcess = new SnowFlakeAutoIdProcess(field);
                                snowFlakeAutoIdProcess.setPrimaryKey(autoId.primaryKey());
                                finalIdProcesses.add(snowFlakeAutoIdProcess);
                                break;
                        }
                    }
                });


public class SnowFlakeAutoIdProcess extends BaseAutoIdProcess {

    private static Snowflake snowflake = IdUtil.createSnowflake(0,0);


    public SnowFlakeAutoIdProcess(Field field) {
        super(field);
    }

    @Override
    void setFieldValue(Object entity) throws Exception{
        long value = snowflake.nextId();
        field.set(entity,value);
    }
}

如果项目中的mapper.xml已经的insert语句已经含有id,比如

insert into sys_test( `id`,`type`, `url`,`menu_type`,`gmt_create`)values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})

则只需到填充id值这一步。拦截器的任务就完成。如果mapper.xml的insert不含id,形如

insert into sys_test( `type`, `url`,`menu_type`,`gmt_create`)values( #{type}, #{url},#{menuType},#{gmtCreate})

则还需重写insert语句以及新增id参数

4、重写insert语句以及新增id参数(可选)

4.1 重写insert语句

方法一: 从 MappedStatement 对象中获取 SqlSource 对象,再从从 SqlSource 对象中获取获取 BoundSql 对象,通过 BoundSql#getSql 方法获取原始的sql,最后在原始sql的基础上追加id

方法二:

引入

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

通过

com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser

获取相应的表名、需要insert的字段名。然后重新拼凑出新的insert语句

4.2 把新的sql重置给Invocation

其核心实现思路是创建一个新的MappedStatement,新的MappedStatement绑定新sql,再把新的MappedStatement赋值给Invocation的args[0],代码片段如下

 private void resetSql2Invocation(Invocation invocation, BoundSqlHelper boundSqlHelper,Object entity) throws SQLException {
        final Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        MappedStatement newStatement = newMappedStatement(statement, new BoundSqlSqlSource(boundSqlHelper));
        MetaObject msObject =  MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
        msObject.setValue("sqlSource.boundSqlHelper.boundSql.sql", boundSqlHelper.getSql());

            args[0] = newStatement;

    }

4.3 新增id参数

其核心是利用

org.apache.ibatis.mapping.ParameterMapping

核心代码片段如下

  private void setPrimaryKeyParaterMapping(String primaryKey) {
           ParameterMapping parameterMapping = new ParameterMapping.Builder(boundSqlHelper.getConfiguration(),primaryKey,boundSqlHelper.getTypeHandler()).build();
           boundSqlHelper.getBoundSql().getParameterMappings().add(parameterMapping);
       }

5、将mybatis拦截器注入到spring容器

可以直接在拦截器上加

@org.springframework.stereotype.Component

注解。也可以通过

 @Bean
    public AutoIdInterceptor autoIdInterceptor(){
        return new AutoIdInterceptor();
    }

6、在需要实现自增主键的实体字段上加如下注解

@AutoId(primaryKey = "id")
    private Long id;

测试

1、对应的测试实体以及单元测试代码如下

@Data
public class TestDO implements Serializable {
    private static final long serialVersionUID = 1L;

    @AutoId(primaryKey = "id")
    private Long id;
    private Integer type;
    private String url;
    private Date gmtCreate;
    private String menuType;
}


@Autowired
    private TestService testService;

    @Test
    public void testAdd(){
        TestDO testDO = new TestDO();
        testDO.setType(1);
        testDO.setMenuType("1");
        testDO.setUrl("www.test.com");
        testDO.setGmtCreate(new Date());
        testService.save(testDO);
        testService.get(110L);
    }

    @Test
    public void testBatch(){
        List<TestDO> testDOList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            TestDO testDO = new TestDO();
            testDO.setType(i);
            testDO.setMenuType(i+"");
            testDO.setUrl("www.test"+i+".com");
            testDO.setGmtCreate(new Date());
            testDOList.add(testDO);
        }

        testService.saveBatch(testDOList);
    }

2、当mapper的insert语句中含有id,形如下

<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
        insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`)
        values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
    </insert>

以及批量插入sql

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
        insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`)
        values
        <foreach collection="list" item="test" index="index" separator=",">
            ( #{test.id},#{test.gmtCreate},#{test.type}, #{test.url},
            #{test.menuType})
        </foreach>
    </insert>

查看控制台sql打印语句

15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`) values( ?,?, ?,?,? ) 
15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 356829258376544258(Long), 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:52:04.738(Timestamp)
15:52:04 [main] DEBUG com.nlybgeek.dao.TestDao.save - <==    Updates: 1


15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`) values ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?) 
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 356829258896637961(Long), 2020-09-11 15:52:04.847(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356829258896637960(Long), 2020-09-11 15:52:04.847(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356829258896637962(Long), 2020-09-11 15:52:04.847(Timestamp), 2(Integer), www.test2.com(String), 2(String)
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3

查看数据库

springboot2结合mybatis拦截器实现主键自动生成

3、当mapper的insert语句中不含id,形如下

<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
        insert into sys_test(`type`, `url`,`menu_type`,`gmt_create`)
        values(#{type}, #{url},#{menuType},#{gmtCreate})
    </insert>

以及批量插入sql

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
        insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`)
        values
        <foreach collection="list" item="test" index="index" separator=",">
            (#{test.gmtCreate},#{test.type}, #{test.url},
            #{test.menuType})
        </foreach>
    </insert>

查看控制台sql打印语句

15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`type`,`url`,`menu_type`,`gmt_create`,id) values (?,?,?,?,?) 
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:59:46.741(Timestamp), 356831196144992264(Long)
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - <==    Updates: 1


15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`,id) values (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?) 
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 2020-09-11 15:59:46.845(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356831196635725829(Long), 2020-09-11 15:59:46.845(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356831196635725828(Long), 2020-09-11 15:59:46.845(Timestamp), 2(Integer), www.test2.com(String), 2(String), 356831196635725830(Long)
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3

从控制台我们可以看出,当mapper.xml没有配置id字段时,则拦截器会自动帮我们追加id字段

查看数据库

springboot2结合mybatis拦截器实现主键自动生成

总结

本文虽然是介绍mybatis拦截器实现主键自动生成,但文中更多讲解如何实现一个拦截器以及主键生成思路,并没把intercept实现主键方法贴出来。其原因主要是主键自动生成在mybatis-plus里面就有实现,其次是有思路后,大家就可以自己实现了。最后对具体实现感兴趣的朋友,可以查看文末中demo链接

参考文档

mybatis拦截器 mybatis插件实现自定义改写表名 mybatis拦截器,动态修改sql语句

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-mybatis-autoId

点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
MySQL使用on duplicate key update时导致主键不连续自增
使用onduplicatekeyupdate语法有时是很方便,但是会有一个影响:默认情况下,每次更新都会更新该表的自增主键ID,如果更新频率很快,会导致主键ID自增的很快,过段时间就超过数字类型的的范围了解决这个问题,有两种方式:(实际我目前使用的方式是把自增主键ID设置为bigint,也有一部分操作先查询再选择插入OR更新)方法一:拆分成两个
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
MySql 面试开发技术点汇总
表结构设计1、为什么一定要设一个主键?答:因为你不设主键的情况下,innodb也会帮你生成一个隐藏列,作为自增主键。所以啦,反正都要生成一个主键,那你还不如自己指定一个主键,在有些情况下,就能显式的用上主键索引,提高查询效率!2、你们主键是用自增还是UUID?答:肯定答自增啊。innod
Wesley13 Wesley13
3年前
mysql 未定义主键,mysql 如何处理?
InnoDB会自动帮你创建一个不可见的、长度为6字节的row\_id,而且InnoDB维护了一个全局的dictsys.row\_id,所以未定义主键的表都会共享该row\_id,每次插入一条数据都把全局row\_id当成主键id,然后全局row\_id加1。该全局row\_id在代码实现上使用的事bigintunsigned类型,但实际上只给row\_
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
PostgreSQL 主键自动增长
建立主键并设置自动增加的办法好好几种,这里记录我测试过的:droptablepro_process;CREATETABLE"public"."pro_process"("id"SERIALprimarykey,//设置主键并自动增长"county_code"varchar(6)COLLATE
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Vitess全局唯一ID生成的实现方案 | 京东云技术团队
为了标识一段数据,通常我们会为其指定一个唯一id,比如利用MySQL数据库中的自增主键。但是当数据量非常大时,仅靠数据库的自增主键是远远不够的,并且对于分布式数据库只依赖MySQL的自增id无法满足全局唯一的需求。因此,产生了多种解决方案,如UUID,Sn
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k