springboot2結合mybatis攔截器實現主鍵自動生成

前言

前陣子和朋友聊天,他說他們項目有個需求,要實現主鍵自動生成,不想每次新增的時候,都手動設置主鍵。因而我就問他,那大家數據庫表設置主鍵自動遞增不就得了。他的回答是他們項目目前的id都是採用雪花算法來生成,所以爲了項目穩定性,不會切換id的生成方式。html

朋友問我有沒有什麼實現思路,他們公司的orm框架是mybatis,我就建議他說,否則讓你老大把mybatis切換成mybatis-plus。mybatis-plus就支持註解式的id自動生成,並且mybatis-plus只是對mybatis進行加強不作改變。朋友仍是那句話,說爲了項目穩定,以前項目組沒有使用mybatis-plus的經驗,貿然切換不知道會不會有什麼坑。後面沒招了,我就跟他說否則你用mybatis的攔截器實現一個吧。因而又有一篇吹水的創做題材出現。java

前置知識

在介紹如何經過mybatis攔截器實現主鍵自動生成以前,咱們先來梳理一些知識點mysql

一、mybatis攔截器的做用

mybatis攔截器設計的初衷就是爲了供用戶在某些時候能夠實現本身的邏輯而沒必要去動mybatis固有的邏輯git

二、Interceptor攔截器

每一個自定義攔截器都要實現github

org.apache.ibatis.plugin.Interceptor

這個接口,而且自定義攔截器類上添加@Intercepts註解算法

三、攔截器能攔截哪些類型
  • Executor:攔截執行器的方法。
  • ParameterHandler:攔截參數的處理。
  • ResultHandler:攔截結果集的處理。
  • StatementHandler:攔截Sql語法構建的處理。
四、攔截的順序

a、不一樣類型攔截器的執行順序spring

Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

b、多個攔截器攔截同種類型同一個目標方法,執行順序是後配置的攔截器先執行sql

好比在mybatis配置以下數據庫

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

則InterceptorB先執行。apache

若是是和spring作了集成,先注入spring ioc容器的攔截器,則後執行。好比有個mybatisConfig,裏面有以下攔截器bean配置

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

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

則InterceptorB先執行。固然若是你是直接用@Component註解這形式,則能夠配合@Order註解來控制加載順序

五、攔截器註解介紹

@Intercepts:標識該類是一個攔截器

@Signature:指明自定義攔截器須要攔截哪個類型,哪個方法。
@Signature註解屬性中的type表示對應能夠攔截四種類型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的一種;method表示對應類型(Executor、ParameterHandler、ResultHandler、StatementHandler)中的哪類方法;args表示對應method中的參數類型

六、攔截器方法介紹

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配置文件中指定一些屬性

主鍵自動生成思路

一、定義一個攔截器

主要攔截

`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 {}

二、判斷sql操做類型

Executor 提供的方法中,update 包含了 新增,修改和刪除類型,沒法直接區分,須要藉助 MappedStatement 類的屬性 SqlCommandType 來進行判斷,該類包含了全部的操做類型

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

當SqlCommandType類型是insert咱們才進行主鍵自增操做

三、填充主鍵值

3.一、編寫自動生成id註解
Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
    /**
     * 主鍵名
     * @return
     */
    String primaryKey();

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

    enum IdType{
        SNOWFLAKE
    }
}
3.二、 雪花算法實現

咱們能夠直接拿hutool這個工具包提供的idUtil來直接實現算法。

引入

<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
Snowflake snowflake = IdUtil.createSnowflake(0,0);
long value = snowflake.nextId();
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參數

四、重寫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);
       }
五、將mybatis攔截器注入到spring容器

能夠直接在攔截器上加

@org.springframework.stereotype.Component

註解。也能夠經過

@Bean
    public AutoIdInterceptor autoIdInterceptor(){
        return new AutoIdInterceptor();
    }
六、在須要實現自增主鍵的實體字段上加以下註解
@AutoId(primaryKey = "id")
    private Long id;

測試

一、對應的測試實體以及單元測試代碼以下
@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);
    }
二、當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

查看數據庫
sql語句.png

三、當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字段

查看數據庫
sql語句1.png

總結

本文雖然是介紹mybatis攔截器實現主鍵自動生成,但文中更多講解如何實現一個攔截器以及主鍵生成思路,並沒把intercept實現主鍵方法貼出來。其緣由主要是主鍵自動生成在mybatis-plus裏面就有實現,其次是有思路後,你們就能夠本身實現了。最後對具體實現感興趣的朋友,能夠查看文末中demo連接

參考文檔

mybatis攔截器
mybatis插件實現自定義改寫表名
mybatis攔截器,動態修改sql語句

demo連接

https://github.com/lyb-geek/s...
相關文章
相關標籤/搜索