mybatis插件开发

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内部的查询方法不同,分为BatchExecutorReuseExecutorSimpleExecutor以及CachingExecutor
  • executorquery方法将真正的查询交给具体实现类的doQuery来执行
  • doquery中会使用到的StatementHandler用于封装处理jdbcstatementResultHandler用于处理结果集。最后将结果返回为一个List<Object>selectOne调用的还是SelectList,只是在取结果集的时候,返回了第一个元素。

工作流程图:

image

源码体现方式

openSession:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//打开对应数据源的Session
//ExecutorType包含三种SIMPLE,REUSE,BATCH(分别对应三种Executor),level为事务级别,autocommit自动提交
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//获取数据源相关信息
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

//根据execTyoe创建对应的Executor(工厂模式),具体代码见下方
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

获取Executor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//根据executorType创建执行器
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//这里的PluginAll()方法,即执行executor下的拦截器,后面会提到
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

相关Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public 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
2
3
4
5
6
7
8
9
10
11
12
//拦截器链,该类维护了一个实现Interceptor的集合,调用pluginAll时,会依次调用对应的拦截器。
public class InterceptorChain {

private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
}

插件(拦截器)开发

说是插件,其实就是类似拦截器一般的功能。在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
2
3
4
5
6
7
public interface Interceptor {
Object intercept(Incocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);
}

setProperties主要是给拦截器提供参数用的,使用方式简单,此处不再介绍。

再来看plugin方法,其参数为target,即拦截器所要拦截的对象。前面说到InterceptorChain维护了一个Interceptor的集合,这里的plugin方法实际是在对应的拦截位置,由InterceptorChain进行循环调用时触发。实现类直接通过如下方式使用:

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

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
2
3
//以拦截参数处理器ParameterHandler的setParameters为例
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters",
args = {PreparedStatement.class})})

@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})

除以上之外,还有getTransactionisClosedcloseflushStatementsqueryCursor等方法可以拦截,但是即应用不常见,此处略过。

拦截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
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
/**
* MyBatis Map 类型下划线 Key 转小写驼峰形式
*
* @author liuzenghui
*/
//返回值类型为Map,映射字段中的下划线为驼峰,由于是对返回值做拦截,所以这里签名指定ResultSetHandler
//方法为handleResultSets,处理除存储过程和返回值为Cursor以外的所有结果
@Intercepts(
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
)
@SuppressWarnings({ "unchecked", "rawtypes" })
public class CameHumpInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
//先执行得到结果,再对结果进行处理
List<Object> list = (List<Object>) invocation.proceed();
for(Object object : list){
//如果结果是 Map 类型,就对 Map 的 Key 进行转换
if(object instanceof Map){
processMap((Map)object);
} else {
break;
}
}
return list;
}

/**
* 处理 Map 类型
*
* @param map
*/
private void processMap(Map<String, Object> map) {
Set<String> keySet = new HashSet<String>(map.keySet());
for(String key : keySet){
//大写开头的会将整个字符串转换为小写,如果包含下划线也会处理为驼峰
if((key.charAt(0) >= 'A' && key.charAt(0) <= 'Z') || key.indexOf("_") >= 0){
Object value = map.get(key);
map.remove(key);
map.put(underlineToCamelhump(key), value);
}
}
}

/**
* 将下划线风格替换为驼峰风格
*
* @param inputString
* @return
*/
public static String underlineToCamelhump(String inputString) {
StringBuilder sb = new StringBuilder();

boolean nextUpperCase = false;
for (int i = 0; i < inputString.length(); i++) {
char c = inputString.charAt(i);
if(c == '_'){
if (sb.length() > 0) {
nextUpperCase = true;
}
} else {
if (nextUpperCase) {
sb.append(Character.toUpperCase(c));
nextUpperCase = false;
} else {
sb.append(Character.toLowerCase(c));
}
}
}
return sb.toString();
}

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

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

项目中所用到的例子

拦截Executor接口的updatequery方法,对添加了自定义注解@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 接口默认方法
}

以上です。

0%