Mybatis整体工作流程介绍
一个标准的mybatis
查询通常如下所示,此处不考虑整合spring
,总体思想是类似的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17@Test
public void testSelectAll() throws IOException {
Reader reader = null;
SqlSession sqlSession = null;
try {
reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
sqlSession = sqlSessionFactory.openSession();
List<Country> countryList = sqlSession.selectList("selectAll");
printCountryList(countryList);
} finally {
if (reader != null)
reader.close();
if (sqlSession != null)
sqlSession.close();
}
}
- 获取
Mybatis
的配置文件,内部通过ClassLoader
加载文件流,这一步需要对Classloader
有一定的理解 - 创建
SqlSessionFactory
, 通过JDK内部的w3c解析配置文件的内容,封装到Configration
对象中,最后通过Configuration
来创建DefaultSqlSessionFactory
. - 通过
SqlSessionFactory
创建SqlSession
对象 - 不同的
executor
内部的查询方法不同,分为BatchExecutor
,ReuseExecutor
,SimpleExecutor
以及CachingExecutor
executor
的query
方法将真正的查询交给具体实现类的doQuery
来执行doquery
中会使用到的StatementHandler
用于封装处理jdbc
的statement
,ResultHandler
用于处理结果集。最后将结果返回为一个List<Object>
,selectOne
调用的还是SelectList
,只是在取结果集的时候,返回了第一个元素。
工作流程图:
源码体现方式
openSession:
1 | //打开对应数据源的Session |
获取Executor
1 | //根据executorType创建执行器 |
相关Handler1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//pluginAll
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
//pluginAll
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//pluginAll
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
pluginAll
1 | //拦截器链,该类维护了一个实现Interceptor的集合,调用pluginAll时,会依次调用对应的拦截器。 |
插件(拦截器)开发
说是插件,其实就是类似拦截器一般的功能。在Mybatis
中,可以插入拦截器的地方有以下几个:
- executor (拦截执行器)
- parameterHandler (拦截参数)
- ResultSetHandler (拦截结果集)
- StatementHandler (拦截sql构建)
拦截位置 | 拦截内容 |
---|---|
Executor | query、update、flushStatements、commit、rollback、getTransaction、close、isClosed |
ParameterHandler | getParameterObject、setParameters |
ResultSetHandler | handleResultSets、handleCursorResultSets、handleOutputParameters |
StatementHandler | prepare、parameterize、batch、update、query |
在之前的newExecutor
方法,以及各种handler
处理器的地方提到过PluginAll
方法,其实就是对应的这几个位置。
接口介绍
Mybatis插件通过实现拦截器接口Interceptor来完成,原接口如下:
1 | public interface Interceptor { |
setProperties
主要是给拦截器提供参数用的,使用方式简单,此处不再介绍。
再来看plugin
方法,其参数为target
,即拦截器所要拦截的对象。前面说到InterceptorChain
维护了一个Interceptor
的集合,这里的plugin
方法实际是在对应的拦截位置,由InterceptorChain
进行循环调用时触发。实现类直接通过如下方式使用:
1 | @Override |
plugin.warp()
方法会自动判断拦截器的签名(接下来会介绍到)和被拦截的接口是否匹配,在两者一致的情况下会通过动态代理拦截该对象(如拦截器的签名为query
,那么在调用query
方法时会被拦截)。
intercept
方法则是拦截器执行拦截逻辑的地方,其参数类型为Invocation
,可以从中获取到很多反射相关的信息,如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 @Override
public Object intercept(Invocation invocation) throws Throwable {
//getArgs返回的是被拦截方法的参数,这里取第一个参数MappedStatement
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
//代理对象
Object target = invocation.getTarget():
//方法名
String methodName = invocation.getMethod().getName();
//获取以上信息后.............在此处完成业务需求(如参数处理,驼峰映射等)
//最后通过invocation.proceed()返回结果
//本质上proceed()方法是调用了method.invoke(target,args)
return invocation.proceed();
}
拦截器签名
在自定义拦截器的过程中,实现Interceptor
只表示声明了一个拦截器,但该拦截器实际在什么位置使用则需要拦截器签名来进行定义。
使用@Intercepts
和签名注解@Signature
来配置拦截器所要拦截的方法。
@Intercepts
注解中的属性是一个@Signature
签名数组,可以在同一个拦截器中同时拦截不同的接口和方法,使用方式如下:
1 | //以拦截参数处理器ParameterHandler的setParameters为例 |
@Signature
包含三个属性:
- type:设置拦截的接口,即
Executor
,ParameterHandler
,ResultSetHandler
,StatementHandler
四者中的一个 - method:设置拦截接口中的方法名,根据前面表格中的对应关系来。
- args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法。
参考刘增辉老师的《Mybatis从入门到精通》
,接下来例举一些较为常用的被拦截方法和接口
(可先看后面的实例,回头再来理解各接口的拦截签名)
拦截Executor接口
Executor
接口包含的几个方法:
- int update(MappedStatedment ms,Object Parameter) throws SQLException
该方法会拦截所有的
INSERT、UPDATE、DELETE
操作,对应的签名为:
1 | @Signature(type = Executor.class, method = "update", args = {MappedStatedment.class,Object.class}) |
List query(MappedStatedment ms,Object parameter,RowBounds rowBounds,ResultHandler resultHandler) throws SQLException 该方法用于拦截所有的
SELECT
查询方法,一般是最常被拦截的方法,对应的签名为::1
@Signature(type = Executor.class, method = "query", args = {MappedStatedment.class,Object.class,RowBounds.class,ResultHandler.class})
void commit(boolean required) throws SQLException
该方法只在通过
sqlsession
调用commit
方法时才被调用,接口方法对应的签名为:1
@Signature(type = Executor.class, method = "commit", args = {boolean.class})
void rollback(boolean required) throws SQLException
该方法只在通过
sqlsession
调用rollback
方法时才被调用,接口方法对应的签名为:1
@Signature(type = Executor.class, method = "rollback", args = {boolean.class})
除以上之外,还有getTransaction
、isClosed
、close
、flushStatements
、queryCursor
等方法可以拦截,但是即应用不常见,此处略过。
拦截ParameterHandler接口
ParameterHandler
接口的方法很少,只有以下两个
Object getParameterObject()
该方法只在执行存储过程处理出参的时候被调用,接口对应的签名如下:
1
@Signature(type = ParameterHandler.class, method = "getParameterObject", args = {})
void setParameters(PreparedStatement var1) throws SQLException
该方法在设置SQL参数时被调用,接口对应的签名如下:
1
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatedment.class})
拦截ResultSetHandler
ResultSetHandler
接口包含如下三个方法:
List handleResultSets(Statement var1) throws SQLException 该方法会拦截除存储过程及返回值类型为
Cursor<T>
以外的查询方法,对应的签名为:1
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
Cursor handleCursorResultSets(Statement var1) throws SQLException 3.4.0新增方法,拦截返回值类型为
Cursor<T>
的方法,对应的签名为:1
@Signature(type = ResultSetHandler.class, method = "handleCursorResultSets", args = {Statement.class})
void handleOutputParameters(CallableStatement var1) throws SQLException
该方法在使用存储过程处理出参时被调用,对应的签名为:
1
@Signature(type = ResultSetHandler.class, method = "handleOutputParameters", args = {CallableStatement.class})
拦截StatementHandler接口
Statement prepare(Connection var1, Integer var2) throws SQLException
在数据库执行前被调用,优于当前接口中的其他方法,对应的签名为:
1
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})
void parameterize(Statement var1) throws SQLException
在
prepare
方法之后执行,用于处理参数,对应的签名为:1
@Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class})
void batch(Statement var1) throws SQLException
在全局设置配置
defaultExecutorType="Batch"
时,操作数据库会执行该方法,对应的签名为:1
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
List query(Statement var1, ResultHandler var2) throws SQLException 执行
SELECT
方法时被调用,对应的签名为:1
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class,ResultHandler.class})
Cursor queryCursor(Statement var1) throws SQLException 3.4.0新增方法,在返回值类型为
Cursor<T>
的查询中被调用,对应的签名为:1
@Signature(type = StatementHandler.class, method = "queryCursor", args = {Statement.class})
拦截器实例
这里先介绍一个刘增辉老师书本上的例子,再介绍一个项目中实际使用到的场景。
下划线转驼峰插件
1 | /** |
项目中所用到的例子
拦截Executor
接口的update
和query
方法,对添加了自定义注解@DefaultParamsInsert
的方法方法进行默认参数追加。
(这里的作用类似于新增一条记录时。默认加上录入人id、行政区划、单位等)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@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class ParamsInterceptor implements Interceptor {
@Autowired
private CommonUtil commonUtil;
@Value("${system.xzqh}")
private String xzqh;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//从invocation获取需要的信息
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
String runMethod = "";
if (sqlId.indexOf('.') > -1) {
runMethod = sqlId.substring(sqlId.lastIndexOf('.') + 1);
}
String className = sqlId.substring(0, sqlId.lastIndexOf('.'));
String methodName = invocation.getMethod().getName();
Method[] method = Class.forName(className).getMethods();
//进行逻辑处理
for (Method m : method) {
// 找到需注入默认值的接口方法,即添加了自定义注解@DefaultParamsInsert的方法
Annotation annotation = m.getAnnotation(DefaultParamsInsert.class);
if (annotation != null && StringUtils.equals(methodName, "update") && m.getName().equals(runMethod)) {
Object parameter = invocation.getArgs()[1];
// 注入对象值
setProperty(parameter);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
*
* ParamsInterceptor
*
* @description 设置需要侵入的key-value
* @param obj 注入值的对象
*/
private void setProperty(Object obj) {
if (obj != null && commonUtil != null) {
User user = commonUtil.getCurrentUser();
Corp corp = commonUtil.getCurrentCorp();
if (user != null) {
try {
if (BeanUtils.getProperty(obj, "ccjr") == null) {
BeanUtils.setProperty(obj, "ccjr", user.getId());
}
if (BeanUtils.getProperty(obj, "csjly") == null) {
BeanUtils.setProperty(obj, "csjly", xzqh);
}
if (BeanUtils.getProperty(obj, "ccorp") == null) {
BeanUtils.setProperty(obj, "ccorp", user.getCorpId());
}
if (BeanUtils.getProperty(obj, "cdept") == null) {
BeanUtils.setProperty(obj, "cdept", user.getDeptId());
}
} catch (Exception e) {
log.warn("设置需要侵入的key-value error", e);
}
}
}
}
/**
*
* @see org.apache.ibatis.plugin.Interceptor#setProperties(java.util.Properties)
*/
@Override
public void setProperties(Properties properties) {
// Interceptor 接口默认方法
}
以上です。