小伙伴们元宵节高兴,记得吃元宵哦~
在日常开发中,小伙伴们多多少少都有用过 MyBatis 插件,松哥猜想大家用的最多的就是 MyBatis 的分页插件!不晓得小伙伴们有没有想过有一天本人也来开发一个 MyBatis 插件?
其实本人入手撸一个 MyBatis 插件并不难,明天松哥就把手带大家撸一个 MyBatis 插件!
1.MyBatis 插件接口
即便你没开发过 MyBatis 插件,预计也能猜出来,MyBatis 插件是通过拦截器来起作用的,MyBatis 框架在设计的时候,就曾经为插件的开发预留了相干接口,如下:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
这个接口中就三个办法,第一个办法必须实现,前面两个办法都是可选的。三个办法作用别离如下:
- intercept:这个就是具体的拦挡办法,咱们自定义 MyBatis 插件时,个别都须要重写该办法,咱们插件所实现的工作也都是在该办法中实现的。
- plugin:这个办法的参数 target 就是拦截器要拦挡的对象,一般来说咱们不须要重写该办法。Plugin.wrap 办法会主动判断拦截器的签名和被拦挡对象的接口是否匹配,如果匹配,才会通过动静代理拦挡指标对象。
- setProperties:这个办法用来传递插件的参数,能够通过参数来扭转插件的行为。咱们定义好插件之后,须要对插件进行配置,在配置的时候,能够给插件设置相干属性,设置的属性能够通过该办法获取到。插件属性设置像上面这样:
<plugins>
<plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
<property name="xxx" value="xxx"/>
</plugin>
</plugins>
2.MyBatis 拦截器签名
拦截器定义好了后,拦挡谁?
这个就须要拦截器签名来实现了!
拦截器签名是一个名为 @Intercepts 的注解,该注解中能够通过 @Signature 配置多个签名。@Signature 注解中则蕴含三个属性:
- type: 拦截器须要拦挡的接口,有 4 个可选项,别离是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
- method: 拦截器所拦挡接口中的办法名,也就是后面四个接口中的办法名,接口和办法要对应上。
- args: 拦截器所拦挡办法的参数类型,通过办法名和参数类型能够锁定惟一一个办法。
一个简略的签名可能像上面这样:
@Intercepts(@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
))
public class CamelInterceptor implements Interceptor {
//...
}
3.被拦挡的对象
依据后面的介绍,被拦挡的对象次要有如下四个:
Executor
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
各办法含意别离如下:
- update:该办法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦挡这些操作,能够通过该办法实现。
- query:该办法会在 SELECT 查询方法执行时被调用,办法参数携带了很多有用的信息,如果须要获取,能够通过该办法实现。
- queryCursor:当 SELECT 的返回类型是 Cursor 时,该办法会被调用。
- flushStatements:当 SqlSession 办法调用 flushStatements 办法或执行的接口办法中带有 @Flush 注解时该办法会被触发。
- commit:当 SqlSession 办法调用 commit 办法时该办法会被触发。
- rollback:当 SqlSession 办法调用 rollback 办法时该办法会被触发。
- getTransaction:当 SqlSession 办法获取数据库连贯时该办法会被触发。
- close:该办法在懒加载获取新的 Executor 后会被触发。
- isClosed:该办法在懒加载执行查问前会被触发。
ParameterHandler
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps) throws SQLException;
}
各办法含意别离如下:
- getParameterObject:在执行存储过程解决出参的时候该办法会被触发。
- setParameters:设置 SQL 参数时该办法会被触发。
ResultSetHandler
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
各办法含意别离如下:
- handleResultSets:该办法会在所有的查询方法中被触发(除去返回值类型为 Cursor<E> 的查询方法),一般来说,如果咱们想对查问后果进行二次解决,能够通过拦挡该办法实现。
- handleCursorResultSets:当查询方法的返回值类型为 Cursor<E> 时,该办法会被触发。
- handleOutputParameters:应用存储过程解决出参的时候该办法会被调用。
StatementHandler
public interface StatementHandler {
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
void parameterize(Statement statement)
throws SQLException;
void batch(Statement statement)
throws SQLException;
int update(Statement statement)
throws SQLException;
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
各办法含意别离如下:
- prepare:该办法在数据库执行前被触发。
- parameterize:该办法在 prepare 办法之后执行,用来解决参数信息。
- batch:如果 MyBatis 的全剧配置中配置了
defaultExecutorType=”BATCH”
,执行数据操作时该办法会被调用。 - update:更新操作时该办法会被触发。
- query:该办法在 SELECT 办法执行时会被触发。
- queryCursor:该办法在 SELECT 办法执行时,并且返回值为 Cursor 时会被触发。
在开发一个具体的插件时,咱们该当依据本人的需要来决定到底拦挡哪个办法。
4.开发分页插件
4.1 内存分页
MyBatis 中提供了一个不太好用的内存分页性能,就是一次性把所有数据都查问进去,而后在内存中进行分页解决,这种分页形式效率很低,基本上没啥用,然而如果咱们想要自定义分页插件,就须要对这种分页形式有一个简略理解。
内存分页的应用形式如下,首先在 Mapper 中增加 RowBounds 参数,如下:
public interface UserMapper {
List<User> getAllUsersByPage(RowBounds rowBounds);
}
而后在 XML 文件中定义相干 SQL:
<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
select * from user
</select>
能够看到,在 SQL 定义时,压根不必管分页的事件,MyBatis 会查问到所有的数据,而后在内存中进行分页解决。
Mapper 中办法的调用形式如下:
@Test
public void test3() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
RowBounds rowBounds = new RowBounds(1,2);
List<User> list = userMapper.getAllUsersByPage(rowBounds);
for (User user : list) {
System.out.println("user = " + user);
}
}
构建 RowBounds 时传入两个参数,别离是 offset 和 limit,对应分页 SQL 中的两个参数。也能够通过 RowBounds.DEFAULT 的形式构建一个 RowBounds 实例,这种形式构建进去的 RowBounds 实例,offset 为 0,limit 则为 Integer.MAX_VALUE,也就相当于不分页。
这就是 MyBatis 中提供的一个很不实用的内存分页性能。
理解了 MyBatis 自带的内存分页之后,接下来咱们就能够来看看如何自定义分页插件了。
4.2 自定义分页插件
首先要申明一下,这里松哥带大家自定义 MyBatis 分页插件,次要是想通过这个货色让小伙伴们理解自定义 MyBatis 插件的一些条条框框,理解整个自定义插件的流程,分页插件并不是咱们的目标,自定义分页插件只是为了让大家的学习过程变得乏味一些而已。
接下来咱们就来开启自定义分页插件之旅。
首先咱们须要自定义一个 RowBounds,因为 MyBatis 原生的 RowBounds 是内存分页,并且没有方法获取到总记录数(个别分页查问的时候咱们还须要获取到总记录数),所以咱们自定义 PageRowBounds,对原生的 RowBounds 性能进行加强,如下:
public class PageRowBounds extends RowBounds {
private Long total;
public PageRowBounds(int offset, int limit) {
super(offset, limit);
}
public PageRowBounds() {
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
}
能够看到,咱们自定义的 PageRowBounds 中减少了 total 字段,用来保留查问的总记录数。
接下来咱们自定义拦截器 PageInterceptor,如下:
@Intercepts(@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
))
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
RowBounds rowBounds = (RowBounds) args[2];
if (rowBounds != RowBounds.DEFAULT) {
Executor executor = (Executor) invocation.getTarget();
BoundSql boundSql = ms.getBoundSql(parameterObject);
Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
if (rowBounds instanceof PageRowBounds) {
MappedStatement countMs = newMappedStatement(ms, Long.class);
CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql);
String countSql = "select count(*) from (" + boundSql.getSql() + ") temp";
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
Set<String> keySet = additionalParameters.keySet();
for (String key : keySet) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], countKey, countBoundSql);
Long count = (Long) countQueryResult.get(0);
((PageRowBounds) rowBounds).setTotal(count);
}
CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
pageKey.update("RowBounds");
String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
Set<String> keySet = additionalParameters.keySet();
for (String key : keySet) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql);
return list;
}
//不须要分页,间接返回后果
return invocation.proceed();
}
private MappedStatement newMappedStatement(MappedStatement ms, Class<Long> longClass) {
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType()
);
ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), longClass, new ArrayList<>(0)).build();
builder.resource(ms.getResource())
.fetchSize(ms.getFetchSize())
.statementType(ms.getStatementType())
.timeout(ms.getTimeout())
.parameterMap(ms.getParameterMap())
.resultSetType(ms.getResultSetType())
.cache(ms.getCache())
.flushCacheRequired(ms.isFlushCacheRequired())
.useCache(ms.isUseCache())
.resultMaps(Arrays.asList(resultMap));
if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
return builder.build();
}
}
这是咱们明天定义的外围代码,波及到的知识点松哥来给大家一个一个分析。
- 首先通过 @Intercepts 注解配置拦截器签名,从 @Signature 的定义中咱们能够看到,拦挡的是 Executor#query 办法,该办法有一个重载办法,通过 args 指定了办法参数,进而锁定了重载办法(实际上该办法的另一个重载办法咱们没法拦挡,那个是 MyBatis 外部调用的,这里不做探讨)。
- 将查问操作拦挡下来之后,接下来咱们的操作次要在 PageInterceptor#intercept 办法中实现,该办法的参数重蕴含了拦挡对象的诸多信息。
- 通过
invocation.getArgs()
获取拦挡办法的参数,获取到的是一个数组,失常来说这个数组的长度为 4。数组第一项是一个 MappedStatement,咱们在 Mapper.xml 中定义的各种操作节点和 SQL,都被封装成一个个的 MappedStatement 对象了;数组第二项就是所拦挡办法的具体参数,也就是你在 Mapper 接口中定义的办法参数;数组的第三项是一个 RowBounds 对象,咱们在 Mapper 接口中定义方法时不肯定应用了 RowBounds 对象,如果咱们没有定义 RowBounds 对象,零碎会给咱们提供一个默认的 RowBounds.DEFAULT;数组第四项则是一个解决返回值的 ResultHandler。 - 接下来判断上一步提取到的 rowBounds 对象是否不为 RowBounds.DEFAULT,如果为 RowBounds.DEFAULT,阐明用户不想分页;如果不为 RowBounds.DEFAULT,则阐明用户想要分页,如果用户不想分页,则间接执行最初的
return invocation.proceed();
,让办法持续往下走就行了。 - 如果须要进行分页,则先从 invocation 对象中取出执行器 Executor、BoundSql 以及通过反射拿进去 BoundSql 中保留的额定参数(如果咱们应用了动静 SQL,可能会存在该参数)。BoundSql 中封装了咱们执行的 Sql 以及相干的参数。
- 接下来判断 rowBounds 是否是 PageRowBounds 的实例,如果是,阐明除了分页查问,还想要查问总记录数,如果不是,则阐明 rowBounds 可能是 RowBounds 实例,此时只有分页即可,不必查问总记录数。
- 如果须要查问总记录数,则首先调用 newMappedStatement 办法结构出一个新的 MappedStatement 对象进去,这个新的 MappedStatement 对象的返回值是 Long 类型的。而后别离创立查问的 CacheKey、拼接查问的 countSql,再依据 countSql 构建出 countBoundSql,再将额定参数增加进 countBoundSql 中。最初通过 executor.query 办法实现查问操作,并将查问后果赋值给 PageRowBounds 中的 total 属性。
- 接下来进行分页查问,有了第七步的介绍之后,分页查问就很简略了,这里就不细说了,惟一须要强调的是,当咱们启动了这个分页插件之后,MyBatis 原生的 RowBounds 内存分页会变成物理分页,起因就在这里咱们批改了查问 SQL。
- 最初将查问后果返回。
在后面的代码中,咱们一共在两个中央从新组织了 SQL,一个是查问总记录数的时候,另一个则是分页的时候,都是通过 boundSql.getSql() 获取到 Mapper.xml 中的 SQL 而后进行改装,有的小伙伴在 Mapper.xml 中写 SQL 的时候不留神,结尾可能加上了 ;
,这会导致分页插件从新组装的 SQL 运行出错,这点须要留神。松哥在 GitHub 上看到的其余 MyBatis 分页插件也是一样的,Mapper.xml 中 SQL 结尾不能有 ;
。
如此之后,咱们的分页插件就算是定义胜利了。
5.测试
接下来咱们对咱们的分页插件进行一个简略测试。
首先咱们须要在全局配置中配置分页插件,配置形式如下:
<plugins>
<plugin interceptor="org.javaboy.mybatis03.plugin.PageInterceptor"></plugin>
</plugins>
接下来咱们在 Mapper 中定义查问接口:
public interface UserMapper {
List<User> getAllUsersByPage(RowBounds rowBounds);
}
接下来定义 UserMapper.xml,如下:
<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
select * from user
</select>
最初咱们进行测试:
@Test
public void test3() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
List<User> list = userMapper.getAllUsersByPage(new RowBounds(1,2));
for (User user : list) {
System.out.println("user = " + user);
}
}
这里在查问时,咱们应用了 RowBounds 对象,就只会进行分页,而不会统计总记录数。须要留神的时,此时的分页曾经不是内存分页,而是物理分页了,这点咱们从打印进去的 SQL 中也能看到,如下:
能够看到,查问的时候就曾经进行了分页了。
当然,咱们也能够应用 PageRowBounds 进行测试,如下:
@Test
public void test4() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
PageRowBounds pageRowBounds = new PageRowBounds(1, 2);
List<User> list = userMapper.getAllUsersByPage(pageRowBounds);
for (User user : list) {
System.out.println("user = " + user);
}
System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal());
}
此时通过 pageRowBounds.getTotal() 办法咱们就能够获取到总记录数。
6.小结
好啦,明天次要和小伙伴们分享了咱们如何本人开发一个 MyBatis 插件,插件性能其实都是主要的,最次要是心愿小伙伴们可能了解 MyBatis 的工作流程。
发表回复