Springboot集成mybatis自定义插件开发

mybatis架构

Pasted Graphi

mybatis工作原理

-w777

执行流程:

  1. 读取核心配置文件并返回InputStream流对象。

  2. 根据InputStream流对象解析出Configuration对象,然后创建SqlSessionFactory工厂对象

  3. 根据一系列属性从SqlSessionFactory工厂中创建SqlSession

  4. 从SqlSession中调用Executor执行数据库操作&&生成具体SQL指令

  5. 对执行结果进行二次封装

  6. 提交与事务

mybatis插件简介

mybatis插件就是在执行数据库操作的时候,对于特定方法进行拦截增强,做一些额外的处理的一种方式。
myabtis的插件的增强原理是利用动态代理实现的,可以对数据库操作的执行类做拦截,mybatis主要操作流程如下:

mybatis中的几个操作数据库的执行类是:Executor、StatementHandler、ParameterHandler、ResultSetHandler,其中:

  • Executor 是总的执行者,他就像一个大总管,用于协调管理其他执行者。

  • StatementHandler 拦截Sql语法构建的处理, 是用于生成Statement或者PreparedStatement的执行者,同时他会调用ParameterHandler进行对sql语句中的参数设值,设置完了之后会通过StatementHandler 去调用sql在数据库中执行,最后返回一个结果集,通过ResultSetHandler将结果集和对应的实体进行映射填充数据,之后会把结果实体返回给StatementHandler。

  • ParameterHandler :拦截参数的处理

  • ResultSetHandler :拦截结果集的处理

所以,我们对这几个执行者进行拦截,比如对于StatementHandler 拦截,即是对于sql操作进行拦截,Mybatis自定义插件必须实现Interceptor接口:

1
2
3
4
5
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
  • intercept方法:拦截器具体处理逻辑方法
  • plugin方法:根据签名signatureMap生成动态代理对象
  • setProperties方法:设置Properties属性

下面就对于这个StatementHandler 进行拦截做一个分页实例。

分页插件

需求

拦截数据sql,实现分页功能

实现

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* @author Austin
* @since 2019/9/1 11:37 Sun
*/
@Component
@Intercepts(@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
))
public class PagePlugin implements Interceptor {

@Value("${mybatis.page.plugin.dialect}")
private String dialect;

@Value("${mybatis.page.plugin.pageSqlId}")
private String pageSqlId;

// 插件需要做的事情
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 确定哪些方法需要做分页
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

// 获取原始sql
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
System.out.println("原始sql: " + sql);

MetaObject metaObject = MetaObject.forObject(statementHandler,
SystemMetaObject.DEFAULT_OBJECT_FACTORY,
SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
new DefaultReflectorFactory());

MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

// //sql语句类型 select、delete、insert、update
// String sqlCommandType = mappedStatement.getSqlCommandType().toString();
// 获取mapper接口中的方法名
String mapperMethodName = mappedStatement.getId();

Object paramObj = boundSql.getParameterObject();
if (mapperMethodName.matches(".*ByPage$")) {
Map<String, Object> params = (Map<String, Object>) paramObj;
PageInfo pageInfo = (PageInfo) params.get("page"); // map.put("page", pageInfo)

String countSql = "select count(1) from (" + sql + ") temp ";
System.out.println("查询总数sql: " + countSql);

Connection connection = (Connection) invocation.getArgs()[0];
PreparedStatement countStatement = connection.prepareStatement(countSql);
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
parameterHandler.setParameters(countStatement);
ResultSet rs = countStatement.executeQuery();
if (rs.next()) {
pageInfo.setTotalNumber(rs.getInt(1));
}
rs.close();
countStatement.close();

// 改造sql limit count
String pageSql = this.generatePageSql(sql, pageInfo);
System.out.println("分页sql: " + pageSql);

// 改造后的sql放回
metaObject.setValue("delegate.boundSql.sql", pageSql);
}

// 执行流程提交mybatis
return invocation.proceed();
}

public String generatePageSql(String sql, PageInfo pageInfo) {
StringBuffer sb = new StringBuffer();
if (dialect.equals("mysql")) {
sb.append(sql);
sb.append(" limit " + pageInfo.getStartIndex() + " ," + pageInfo.getTotalSelect());
}
return sb.toString();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}
}

myabtis自定义插件只需要实现Interceptor接口即可,并且注解@Intercepts以及@Signature配置需要拦截的对象,其中

  • type是需要拦截的对象Class,
  • method是对象里面的方法,
  • args是方法参数类型。

注入插件到拦截链

这里有两种方式注入

  • 方式一:直接注入

    1
    Spring boot项目中只需要在拦截器类上加 @Component 注解即可。
  • 方式二:通过myabtis配置加入到拦截链中(多个拦截器时,这种方式可以控制拦截顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@MapperScan({"com.springboot.demo.mapper"})
public class MapperConfig {

//将插件加入到mybatis插件拦截链中
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return new ConfigurationCustomizer() {
@Override
public void customize(Configuration configuration) {
//插件拦截链采用了责任链模式,执行顺序和加入连接链的顺序有关
MyPlugin myPlugin = new MyPlugin();
//设置参数,比如阈值等,可以在配置文件中配置,这里直接写死便于测试
Properties properties = new Properties();
//这里设置慢查询阈值为1毫秒,便于测试
properties.setProperty("time", "1");
myPlugin.setProperties(properties);

configuration.addInterceptor(myPlugin);
}
};
}
}