mybatis插件,实际上是个拦截器,类似spring中的拦截器。思想就是在执行相关sql操作时,拦截并修改要执行的sql。比如分页插件,就是拦截到sql然后在sql中添加分页参数。本文就是通过实现简单的分页插件,来分析插件的编写以及执行过程。
因为编写插件的流程套路都是一样的,下面直接上demo,结合demo分析原理。
1.demo
这里自定义一个插件类,也就是拦截器的实现类:
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class MyMybatisInterceptor implements Interceptor {
private Integer defaultPageIndex = 1;
private Integer defaultPageSize = 10;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//取出被拦截对象
StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(stmtHandler);
// .....省略后续代码
这里先贴一部分代码,因为有几个点需要先说一下。
1.1 核心接口 Interceptor
上述自定义的插件,实现了接口Interceptor。这个接口是mybatis给出的,就是用于编写插件。自定义插件,就是实现这个接口的3个方法,下面看下接口方法:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
- intercept 方法,核心方法,实现这个方法,用来覆盖被拦截对象的原有方法。其参数Invocation对象,用于反射调用原来对象方法。此方法在执行mapper方法时被执行。
- plugin 方法,其入参target是被拦截的对象,此方法实现用来生成目标对象的代理对象。此方法在执行mapper方法时被执行。
- setProperties 方法,用来设置参数,比如pageSize等初始化参数的值。参数值在xml中配置,这个方法在mabatis初始化的时候,会被调用。
1.2 @Intercepts注解
上面demo除了实现了Interceptor接口,还要添加@Intercepts注解,并且写明一些参数内容,比如@Signature等。@Intercepts表示此类是个拦截器,不写此注解,执行会抛异常。
其参数值这里说一下:
@Signature表示签名,就是表明,此拦截器具体拦截哪些对象的哪些内容。
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
- type 表示拦截的对象类型,是四大对象中的一个,具体看下文描述。
- method 表示要拦截的对象的某一种接口方法。
- args 表示要拦截方法的的参数。
a. 拦截的 四大对象类型
- Executor 是执行SQL的全过程,包括组装参数,组装结果集,执行sql过程。一般不会拦击此对象。
- StatementHandler 是执行sql的过程,是最常用的拦截对象,本文demo就是拦截的此对象,可以用来重写执行sql的过程。
- ParameterHandler 主要拦截用于sql执行的参数,可以拦截后重新组装参数。
- ResultSetHandler 用于拦截执行后的结果,我们可以拦截然后重新组装结果。
b. 拦截的方法
上面四种拦截对象确认后,比如我们需要拦截StatementHandler对象,那么接下来,就要指定具体拦截此对象的哪个方法。
在整个mapper执行过程中,是通过Executor调度StatementHandler来完成,比如调度StatementHandler的prepare方法来完成sql的预编译,于是我们需要拦截此prepare方法来修改sql。
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();
}
以上方法都可以被拦截。
c. 拦截的参数
确定了拦截的方法之后,接下来需要指定拦截方法的参数。prepare方法有两个参数:
prepare(Connection connection, Integer transactionTimeout)
因此最终拦截器签名如下:
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
1.3 完整demo
虽然网上demo比较多,但有些还是不能执行的。这里还是贴一下:
MyMybatisInterceptor.java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class MyMybatisInterceptor implements Interceptor {
private Integer defaultPageIndex = 1;
private Integer defaultPageSize = 10;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//取出被拦截对象StatementHandler
//MetaObject是mybatis提供的工具类,利用反射获取对象中的各种内容
StatementHandler stmtHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(stmtHandler);
// 分离代理对象,由于可能存在多个插件,从而形成多次代理,这里通过两次循环得到最原始的被代理类,MyBatis使用的是JDK代理
while (metaObject.hasGetter("h")) {
Object object = metaObject.getValue("h");
metaObject = SystemMetaObject.forObject(object);
}
// 分离最后一个代理对象的目标类
while (metaObject.hasGetter("target")) {
Object object = metaObject.getValue("target");
metaObject = SystemMetaObject.forObject(object);
}
//检查是否是select查询,因为是分页插件,不是select的不用处理
String sql = getSql(metaObject);
if (!checkSelect(sql)) {
// 不是select语句,进入责任链下一层
return invocation.proceed();
}
//得到查询的绑定对象
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
//得到mapper接口参数
Object parameterObject = boundSql.getParameterObject();
PageParams pageParams = getPageParamsForParamObj(parameterObject);
if (pageParams == null) {
// 没有传入page对象,不执行分页处理,进入责任链下一层
return invocation.proceed();
}
// 设置分页默认值
if (pageParams.getPageIndex() == null) {
pageParams.setPageIndex(this.defaultPageIndex);
}
if (pageParams.getPageSize() == null) {
pageParams.setPageSize(this.defaultPageSize);
}
// 设置分页总数,数据总数
setTotalToPage(pageParams, invocation, metaObject, boundSql);
// 校验分页参数
checkPage(pageParams);
return changeSql(invocation, metaObject, boundSql, pageParams);
}
@Override
public Object plugin(Object target) {
// 生成代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 初始化配置的默认页码,无配置则默认1
this.defaultPageIndex = Integer.parseInt(properties.getProperty("default.pageIndex", "1"));
// 初始化配置的默认数据条数,无配置则默认20
this.defaultPageSize = Integer.parseInt(properties.getProperty("default.pageSize", "20"));
}
/**
* 判断是否是select语句
*
* @param sql
* @return
*/
private boolean checkSelect(String sql) {
// 去除sql的前后空格,并将sql转换成小写
sql = sql.trim().toLowerCase();
return sql.indexOf("select") == 0;
}
/**
* 获取分页参数
*
* @param parameterObject
* @return
*/
private PageParams getPageParamsForParamObj(Object parameterObject) throws Exception {
PageParams pageParams = null;
if (parameterObject == null) {
return null;
}
if (parameterObject instanceof Map) {
// 如果传入的参数是map类型的,则遍历map取出Page对象
@SuppressWarnings("unchecked")
Map<String, Object> paramMap = (Map<String, Object>) parameterObject;
Set<String> keySet = paramMap.keySet();
for (String key : keySet) {
Object value = paramMap.get(key);
if (value instanceof PageParams) {
// 返回Page对象
return (PageParams) value;
}
}
} else if (parameterObject instanceof com.example.demo.model.PageParams) {
// 如果传入的是Page类型,则直接返回该对象
return (PageParams) parameterObject;
} else {
Field[] fields = parameterObject.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.getType() == PageParams.class) {
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), parameterObject.getClass());
Method method = pd.getReadMethod();
return (PageParams) method.invoke(parameterObject);
}
}
}
// 初步判断并没有传入Page类型的参数,返回null
return pageParams;
}
/**
* 获取数据总数
*
* @param invocation
* @param metaObject
* @param boundSql
* @return
*/
private int getTotal(Invocation invocation, MetaObject metaObject, BoundSql boundSql) {
// 获取当前的mappedStatement对象
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 获取配置对象
Configuration configuration = mappedStatement.getConfiguration();
// 获取当前需要执行的sql
String sql = getSql(metaObject);
// 改写sql语句,实现返回数据总数 $_paging取名是为了防止数据库表重名
String countSql = "select count(*) as total from (" + sql + ") $_paging";
// 获取拦截方法参数,拦截的是connection对象
Connection connection = (Connection) invocation.getArgs()[0];
PreparedStatement pstmt = null;
int total = 0;
try {
// 预编译查询数据总数的sql语句
pstmt = connection.prepareStatement(countSql);
// 构建boundSql对象
BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(),
boundSql.getParameterObject());
// 构建parameterHandler用于设置sql参数
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(),
countBoundSql);
// 设置sql参数
parameterHandler.setParameters(pstmt);
//执行查询
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
total = rs.getInt("total");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 返回总数据数
return total;
}
/**
* 设置总数据数、总页数
*
* @param page
* @param invocation
* @param metaObject
* @param boundSql
*/
private void setTotalToPage(PageParams page, Invocation invocation, MetaObject metaObject, BoundSql boundSql) {
// 总数据数
int total = getTotal(invocation, metaObject, boundSql);
// 计算总页数
int totalPage = total / page.getPageSize();
if (total % page.getPageSize() != 0) {
totalPage = totalPage + 1;
}
page.setTotal(total);
page.setTotalPage(totalPage);
}
/**
* 校验分页参数
*
* @param page
*/
private void checkPage(PageParams page) {
// 如果当前页码大于总页数,抛出异常
if (page.getPageIndex() > page.getTotalPage()) {
throw new RuntimeException("当前页码[" + page.getPageIndex() + "]大于总页数[" + page.getTotalPage() + "]");
}
// 如果当前页码小于总页数,抛出异常
if (page.getPageIndex() < 1) {
throw new RuntimeException("当前页码[" + page.getPageIndex() + "]小于[1]");
}
}
/**
* 修改当前查询的sql
*
* @param invocation
* @param metaObject
* @param boundSql
* @param page
* @return
*/
private Object changeSql(Invocation invocation, MetaObject metaObject, BoundSql boundSql, PageParams page) throws Exception {
// 获取当前查询的sql
String sql = getSql(metaObject);
// 修改sql,$_paging_table_limit取名是为了防止数据库表重名
String newSql = "select * from (" + sql + ") $_paging_table_limit limit ?, ?";
// 设置当前sql为修改后的sql
setSql(metaObject, newSql);
// 获取PreparedStatement对象
PreparedStatement pstmt = (PreparedStatement) invocation.proceed();
// 获取sql的总参数个数
int parameCount = pstmt.getParameterMetaData().getParameterCount();
// 设置分页参数
pstmt.setInt(parameCount - 1, (page.getPageIndex() - 1) * page.getPageSize());
pstmt.setInt(parameCount, page.getPageSize());
return pstmt;
}
/**
* 获取当前查询的sql
*
* @param metaObject
* @return
*/
private String getSql(MetaObject metaObject) {
return (String) metaObject.getValue("delegate.boundSql.sql");
}
/**
* 设置当前查询的sql
*
* @param metaObject
*/
private void setSql(MetaObject metaObject, String sql) {
metaObject.setValue("delegate.boundSql.sql", sql);
}
}
PageParams.java
@Data
public class PageParams {
private Integer pageIndex;
private Integer pageSize;
private Integer total;
private Integer totalPage;
}
@Mapper
public interface UsersMapper {
List<Users> selectAll(PageParams pageParams);
}
配置信息
@Configuration
public class MybatisConfig {
@Bean
public MyMybatisInterceptor myMybatisInterceptor() {
return new MyMybatisInterceptor();
}
}
如果使用mybatis 的xml配置文件,也可以在mybatis-config.xml配置文件中配置自定义的分页插件:
<plugins>
<plugin interceptor="xx.xxx.xxxx.MyMybatisInterceptor">
<property name="default.pageIndex" value="1"/>
<property name="default.pageSize" value="10"/>
</plugin>
</plugins>
2 插件执行流程
下面分析下,插件是何时如何被加载,然后何时如何被执行的。本文是基于springboot的分析的,因为目前大部分开发工作都是基于springboot。
2.1 加载过程
插件的加载,是在springboot的启动过程中完成的。springboot启动,会初始化maybatis,这个在前面的两篇文章中有分析。下面调重点捋一下流程:
mybatis在springboot中的初始化配置类MybatisAutoConfiguration:
public class MybatisAutoConfiguration {
private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
private final MybatisProperties properties;
//拦截器数组,用于保存初始化加载的所有拦截器
private final Interceptor[] interceptors;
private final ResourceLoader resourceLoader;
private final DatabaseIdProvider databaseIdProvider;
private final List<ConfigurationCustomizer> configurationCustomizers;
public MybatisAutoConfiguration(MybatisProperties properties,
ObjectProvider<Interceptor[]> interceptorsProvider,
ResourceLoader resourceLoader,
ObjectProvider<DatabaseIdProvider> databaseIdProvider,
ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
this.properties = properties;
//这里获取到了所有拦截器
this.interceptors = interceptorsProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = databaseIdProvider.getIfAvailable();
this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}
//后续代码省略。。。。
上述配置类的构造方法中,有如下内容:
this.interceptors = interceptorsProvider.getIfAvailable();参数interceptorsProvider是从参数中传入:
ObjectProvider<Interceptor[]> interceptorsProvider,
这里ObjectProvider是比较关键的,不熟悉的话可以查阅其他资料,简单来说ObjectProvider是spring提供,用来注入bean对象的,其泛型参数是Interceptor[]拦截器数组,也就是用来注入拦截器数组的。执行完interceptorsProvider.getIfAvailable()这个方法,所有实现了Interceptor的拦截器(插件)都会被获取到。
然后,同样在MybatisAutoConfiguration中,在初始化SqlSessionFactory对象的时候,有如下内容:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
//省略若干代码
//。。。。
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
//这里如果拦截器不是空,则set到SqlSessionFactoryBean中
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
//省略若干代码
//。。。。
最终启动代码执行到 SqlSessionFactoryBean#buildSqlSessionFactory() 方法中的时候,有如下内容:
if (!isEmpty(this.plugins)) {
for (Interceptor plugin : this.plugins) {
configuration.addInterceptor(plugin);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registered plugin: '" + plugin + "'");
}
}
}
上述代码,把所有的插件plugin,都add到了Configuration配置对象中保存,打开Configuration#addInterceptor方法:
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
interceptorChain是拦截器链对象,这里也说明,mybatis拦截器执行,使用的是责任链模式。
到这里,所有插件,都被加载保存到了Configuration对象中,这个对象相当于spring的应用上下文,后边sql执行的时候,所有内容都从这个对象中获取。
2.2 执行过程
上面,mybatis已经得到了所有拦截器,并保存到全局应用上下文中。
然后在我们调用mapper的方法时,就会执行。
切入点 plugin 方法
上面说到插件要实现接口Interceptor接口,其中有个plugin方法,我们有如下实现:
@Override
public Object plugin(Object target) {
// 生成代理对象
return Plugin.wrap(target, this);
}
我们就从这个方法切入,分析插件执行前后过程,下面先分析进入这个方法之后的事情,然后在分析如何进入这个方法的。
plugin方法作用
public class Plugin implements InvocationHandler {
.....
}
可以看到此类实现jdk的InvocationHandler,属性jdk动态代理的基本上就已经明白此类的作用了。
这个方法的作用就是生成代理类,代理原有执行对象,本文demo是StatementHandler对象。插件执行的时候,就是先执行了这个plugin方法,然后执行intercept实现方法。
其中Plugin类是mybatis提供给开发者使用的插件类,我们可以直接使用Plugin#wrap实现代理,这样我们就不用自己写那一大坨动态代理的实现代码了,看下Plugin#wrap的具体内容:
public static Object wrap(Object target, Interceptor interceptor) {
//interceptor就是我们自定义的插件对象,这里可以获取到其上的具体的签名信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//target是被代理的原始对象,本demo是StatementHandler对象(实现类是RoutingStatementHandler)
Class<?> type = target.getClass();
//从签名信息中得到接接口信息,本demo是StatementHandler
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//这个if是判断参数target是不是是命中指定的type,是的话就对其代理,否则不处理直接return
if (interfaces.length > 0) {
//jdk动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
- 此方法生成代理类,代理的就是我们在签名中指定的type对象;
- mybatis使用的jdk动态代理生成代理类。
如何进入plugin方法
上面我们已经知道,插件是使用jdk动态代理,代理的是StatementHandler对象的prepare方法,那么当执行prepare方法时,代码就会进入我们的intercept方法中。
StatementHandler#prepare最终是在如下方法中执行(前序方法可以在此打断点查看):
SimpleExecutor#prepareStatement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//这里执行prepare方法,如果有代理,则进入代理类
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
到此,就进入了我们自定义插件(拦截器)中。