「这是我参与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分页的开源项目,对自己写的分页插件进行不断的完善,当然大家也可以在评论区进行交流,共同学习。
本文转载自: 掘金