这里介绍mybatis中插件的原理及使用。将结合源码及示例进行说明。
插件是mybatis的扩展方式,可用于添加默认值、日志记录、黑白名单等。
mybatis插件是通过拦截器实现的,编写插件需要标识拦截方法和实现拦截逻辑。
标识拦截拦截方法是通过注解org.apache.ibatis.plugin.Intercepts和注解org.apache.ibatis.plugin.Signature实现的。
Intercepts注解内是一个Signature注解(每个Signature注解标识了一个需要拦截的方法)列表。Intercepts注解定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
Signature注解位于Intercepts注解内,每个Signature注解标识了一个需要拦截的方法。Signature注解定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
})
public @interface Signature {
//拦截的类对象
Class<?> type();
//拦截的方法名
String method();
//拦截方法的参数定义
Class<?>[] args();
}
mybatis插件可以对Executor、ParameterHandler、ResultSetHandler和StatementHandler接口的实现类进行拦截。
mybatis对上述接口的处理流程如下:
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);
}
ParameterHandler是参数的处理,通过插件可以对参数进行修改设置。
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps)throws SQLException;
}
StatementHandler是执行sql的过程,通过插件可以重写sql。
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();
}
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;
}
mybatis插件需要实现org.apache.ibatis.plugin.Interceptor接口,在方法intercept(Invocation invocation)方法中实现自定义的插件逻辑。Interceptor接口完整定义如下:
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
}
}
mybatis插件实现运用了代理模式和责任链模式。
插件的实现流程如下:
XMLConfigBuilder.pluginElement(XNode parent)方法解析mybatis-config.xml的节点。关键源码如下:
public class XMLConfigBuilder extends BaseBuilder {
//解析plugin节点
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
}
Configuration.addInterceptor(Interceptor interceptor)将拦截器添加到拦截器链(位于Configuration类中)中。关键源码如下:
public class Configuration {
//内部实现是List
protected final InterceptorChain interceptorChain = new InterceptorChain();
//添加拦截器
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
}
通过Configuration.new*系列方法(如: newExecutor,newParameterHandler,newStatementHandler, newResultSetHandler)获取代理对象进行拦截处理。以newExecutor方法为例,关键源码如下:
public class Configuration {
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);
}
//获取拦截执行对象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
InterceptorChain.pluginAll(Object target)方法进行拦截对象链执行。关键源码如下:
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
}
Interceptor.plugin(Object target)执行,关键源码如下:
public interface Interceptor {
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
Plugin.wrap(Object target, Interceptor interceptor)生成拦截对象代理及最终实现Plugin.invoke(Object proxy, Method method, Object[] args),关键源码如下:
public class Plugin implements InvocationHandler {
//生成拦截对象代理
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
//对象代理执行
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}
这里以分页插件为示例。
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.5.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.17version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.6version>
dependency>
package com.study.plugin.bean;
import org.apache.commons.lang3.builder.ToStringBuilder;
public class Stu {
private Long id;
private String name;
//....省略get和set方法
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
package com.study.plugin.bean;
import org.apache.commons.lang3.builder.ToStringBuilder;
import java.util.Collections;
import java.util.List;
public class Page<T> {
private Long pageNo;
private Long pageSize;
private Long total;
public Page() {
}
public Page(Long pageNo, Long pageSize) {
this.pageNo = pageNo;
this.pageSize = pageSize;
}
//....省略get和set方法
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
package com.study.plugin.mapper;
import com.study.plugin.bean.Stu;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface StuMapper {
@Select("select * from stu")
List<Stu> selectStuPage(Page page);
}
package com.study.plugin.plugin;
import com.study.plugin.bean.Page;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import static com.sun.jmx.mbeanserver.Util.cast;
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {
Connection.class, Integer.class})
})
public class PagePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
if (target instanceof StatementHandler) {
StatementHandler statementHandler = cast(target);
Object[] args = invocation.getArgs();
Connection connection = (Connection) args[0];
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
BoundSql boundSql = statementHandler.getBoundSql();
//获取请求参数
Object obj = boundSql.getParameterObject();
Page page = getPage(obj);
if (page == null) {
return invocation.proceed();
}
String sql = boundSql.getSql();
//重写sql
String newSql = sql + " limit " + (page.getPageNo() - 1) * page.getPageSize() + "," + page.getPageSize();
metaObject.setValue("delegate.boundSql.sql", newSql);
//获取总数
long total = getTotal(connection, sql);
page.setTotal(total);
return invocation.proceed();
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
//获取分页参数
Page getPage(Object obj) {
if (obj instanceof Page) {
return cast(obj);
}
if (obj instanceof Map) {
Optional pageOpt = ((Map) obj).values().stream().filter(t -> t instanceof Page).findFirst();
return (Page) pageOpt.get();
}
return null;
}
//获取总数
long getTotal(Connection connection, String sql) {
try {
String countSql = "select count(*) " + sql.substring(sql.indexOf("from"));
PreparedStatement preStat = connection.prepareStatement(countSql);
ResultSet rs = preStat.executeQuery();
if (rs.next()) {
return rs.getLong(1);
}
} catch (Exception e) {
System.out.println(e);
}
return 0L;
}
}
db.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/school
jdbc.username=xx
jdbc.password=xx
mybatis配置文件mybatis-config.xml
<configuration>
<properties resource="db.properties"/>
<plugins>
<plugin interceptor="com.study.plugin.plugin.PagePlugin"/>
plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
dataSource>
environment>
environments>
<mappers>
<package name="com.study.plugin.mapper"/>
mappers>
configuration>
package com.study.plugin;
import com.study.plugin.bean.Page;
import com.study.plugin.bean.Stu;
import com.study.plugin.mapper.StuMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class MybatisPluginStudy {
public static void main(String[] args) throws Exception {
//获取mybatis配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
//获取sqlSession
SqlSession session = factory.openSession();
StuMapper stuMapper = session.getMapper(StuMapper.class);
//方法执行
Page<Stu> page = new Page(2L,2L);
List<Stu> stuList = stuMapper.selectStuPage(page);
System.out.println(page);
stuList.forEach(System.out::println);
session.close();
}
}
输出:
com.study.plugin.bean.Page@543295b0[pageNo=2,pageSize=2,total=7]
com.study.plugin.bean.Stu@202b0582[id=4,name=n4]
com.study.plugin.bean.Stu@235ecd9f[id=5,name=n5]
由于工作时间原因,这里对mybatis插件进行了大体介绍,或有疏漏,后期有再进行完善。