「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」
MyBatis可谓是Java开发工程师必须要掌握的持久层框架,它能够让我们更容易的通过Java代码操作数据库,并且它还有很高的扩展性,我们可以自定义插件,去让MyBatis的功能变的更为强大,本篇文章我们就以打印SQL,SQL分页为例,来讲一下如何开发MyBatis的插件。
前言
如果大家对MyBatis源码不熟悉,可以阅读我的这篇文章,专门讲解MyBatis源码阅读的juejin.cn/post/701763…
如果大家想知道MyBatis插件怎么融入实际项目,请参考我的开源项目gitee.com/zhuhuijie/b…
插件部分位于base-platform/base-common/common-db-mysql下
感兴趣的点个star,持续更新中…
MyBatis 四大内置对象
- Executor 执行器 实际用来执行SQL的对象
 - StatementHandler 数据库会话处理器 编译/处理SQL语句的
 
+ PreparedStatementHanler 创建PreparedStatement 最常用占位符
+ CallableStatementHandler 创建CallableStatement 执行存储过程
+ SimpleStatementHanler 创建Statement 字符串拼接,有SQL注入风险- ParameterHandler 参数处理器
 
1  | csharp复制代码public interface ParameterHandler {  | 
- ResultSetHandler 处理结果集
 
1  | java复制代码public interface ResultSetHandler {  | 
MyBatis 执行SQL的过程
- 根据配置,获取SQLSession对象
 - 通过动态代理,获取Mapper的代理对象
 
1  | ini复制代码StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);  | 
- 通过代理对象调用具体SQL
 
1  | ini复制代码Student student = mapper.getStudentById(id);  | 
+ 通过反射调用该方法
+ mapperMethod.execute(sqlSession, args);
    - INSERT sqlSession.insert()
    - UPDATE sqlSession.update()
    - DELETE sqlSession.delete()
    - SELECT sqlSession.select()
        * selectList
        executor.query() 调用CachingExecutor【装饰者模式】 真实使用SimpleExecutor--->父类BaseExcutor.query() ---> doQuery()抽象 -->SimpleExecutor.doQuery() 【模板模式】
            + Handler对象初始化
                - 创建一个委托,根据不同StatementType创建不同的对象new PreparedStatementHanler()
                    * JDBC的Statement stmt = preparedStatementHanler.instantiateStatement() ---> connection.preparedStatement()
                    * handler.parameterize(stmt) 参数处理
                        + ParameterHandler
            + resultSetHandler.handlerResultSets(preparedStatement) 封装结果
        * ...
+ 得到结果MyBatis 插件如何开发
MyBatis插件本质上就是对MyBatis四大内置对象的增强。
它是基于MyBatis的拦截器,通过AOP的方式进行使用。
案例一 打印SQL插件:
- 创建拦截器
 
注意拦截器实现的是ibatis包下的,上边的注解决定了我们的拦截器是从MyBatis的哪里进行切入的,然后通过AOP的方式进行扩展。
1  | ini复制代码package com.zhj.common.db.mysql.plugins;  | 
- 让该插件生效
 
1  | kotlin复制代码package com.zhj.common.db.mysql.config;  | 
- 通过配置决定是否启用插件
 
@ConditionalOnProperty(value = “zhj.plugins.printSql.enable”, havingValue = “true”, matchIfMissing = false)
- 导入依赖,创建Bean使插件在配置时可以自动提示
 
1  | kotlin复制代码package com.zhj.common.db.mysql.entity;  | 
依赖:
1  | xml复制代码<dependency>  | 
- 配置文件中开启插件:
 
1  | yaml复制代码zhj:  | 
案例二 分页插件:
基础分页插件的实现:
- 创建分页对象
 
1  | java复制代码package com.zhj.common.db.mysql.page;  | 
- 创建分页工具
 
这里我们通过ThreadLocal来设置分页对象
1  | csharp复制代码package com.zhj.common.db.mysql.page;  | 
- 创建实现分页插件的拦截器
 
1  | java复制代码package com.zhj.common.db.mysql.plugins;  | 
- 由于使用代理模式对MyBatis四大内置对象进行增强,当创建多个分页插件时会进行干扰,我们有时候获得的目标对象,并不是真实的目标对象,而是其它插件形成的代理对象,我们需要写一个工具类获取真实的目标对象。
 
1  | java复制代码package com.zhj.common.db.mysql.util;  | 
- 注入分页插件,使其生效
 
1  | kotlin复制代码package com.zhj.common.db.mysql.config;  | 
- 在Controller(Service)中设置开启分页
 
1  | kotlin复制代码package com.zhj.business.controller;  | 
让分页插件更优雅:
- 将侵入部分去掉,通过AOP的方式开启分页,并将分页信息返回
 
1  | ini复制代码package com.zhj.common.db.mysql.aop;  | 
1  | kotlin复制代码package com.zhj.common.db.mysql.config;  | 
- 通过注解将分页的粒度控制到更细的粒度
 
+ 创建注解
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.zhj.common.db.mysql.annotation;
import java.lang.annotation.*;
/**
 * @author zhj
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Page {
}
+ Page对象增加开关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码package com.zhj.common.db.mysql.page;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
 * 分页信息对象
 * @author zhj
 */
@Data
@Accessors(chain = true)
public class Page implements Serializable {
    /**
     * 当前页
     */
    private Integer pageNo;
    /**
     * 每页多少条
     */
    private Integer pageSize;
    /**
     * 总页码
     */
    private Integer pageTotal;
    /**
     * 总条数
     */
    private Integer pageCount;
    /**
     * 是否开启分页
     */
    @JsonIgnore
    private boolean enable;
}
+ 在原来的分页拦截器上增加判断条件
1
2
3
4
5
ini复制代码// 获取分页参数
Page page = PageUtils.getPage();
if (page == null || !page.isEnable()) {
    return invocation.proceed();
}
+ 通过AOP设置开关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
kotlin复制代码package com.zhj.common.db.mysql.aop;
import com.zhj.common.db.mysql.page.Page;
import com.zhj.common.db.mysql.page.PageUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
 * @author zhj
 */
@Aspect
public class PageAOP {
    @Around("@annotation(com.zhj.common.db.mysql.annotation.Page)")
    public Object pageAOP(ProceedingJoinPoint joinPoint) throws Throwable {
        Page page = PageUtils.getPage();
        if (page != null) {
            page.setEnable(true);
        }
        try {
            return joinPoint.proceed();
        } finally {
            if (page != null) {
                page.setEnable(false);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
typescript复制代码package com.zhj.common.db.mysql.config;
import com.zhj.common.db.mysql.aop.PageAOP;
import com.zhj.common.db.mysql.aop.WebPageAOP;
import com.zhj.common.db.mysql.plugins.PagePlugins;
import com.zhj.common.db.mysql.plugins.PrintSQLPlugins;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
 * @author zhj
 */
@Configuration
@MapperScan("com.zhj.data.mapper")
@EnableTransactionManagement
public class DBAutoConfiguration {
    @Bean
    @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false)
    public PrintSQLPlugins getPrintSQLPlugins(){
        return new PrintSQLPlugins();
    }
    @Bean
    public PagePlugins getPagePlugins(){
        return new PagePlugins();
    }
    @Bean
    public WebPageAOP getWebPageAOP(){
        return new WebPageAOP();
    }
    @Bean
    public PageAOP getPageAOP(){
        return new PageAOP();
    }
}
+ 在对应的service或者dao上开启分页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scala复制代码package com.zhj.business.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhj.business.service.StudentService;
import com.zhj.common.db.mysql.annotation.Page;
import com.zhj.data.entity.example.Student;
import com.zhj.data.mapper.example.dao.StudentDao;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * @author zhj
 */
@Service
public class StudentServiceImpl extends ServiceImpl<StudentDao, Student> implements StudentService {
    @Page
    @Override
    public List<Student> list() {
        return super.list();
    }
}
MyBatis插件开发总结
想要对框架进行扩展,首先必须得了解框架源码,只有对源码有较为深入的了解,我们才能更好的把握从哪个点进行切入扩展。本文中的两个案例都是最为简单的实现,说实话,还有很多漏洞,比如第一个打印SQL的插件我们并没有去将参数填充,也没有拿到参数,第二个案例分页,只能满足一些比较简单的场景,如果SQL过于复杂,很可能会出现Bug。这些内容都需要我们不断去学习源码,不断的去学习开源项目,积累的越多,我们写出来的工具越完美。大家可以参考GitHub上MyBatis分页的开源项目,对自己写的分页插件进行不断的完善,当然大家也可以在评论区进行交流,共同学习。
本文转载自: 掘金