MyBatis插件原理
MyBatis对开发者非常友好,它通过提供插件机制,让我们可以根据自己的需要去增强MyBatis的功能。其底层是使用了代理模式+责任链模式
MyBatis官方https://mybatis.org/mybatis-3/zh/configuration.html#plugins可以看到MyBatis允许使用插件来拦截的方法调用
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) //拦截执行器的方法
- ParameterHandler (getParameterObject, setParameters) //拦截参数的处理
- ResultSetHandler (handleResultSets, handleOutputParameters) //拦截结果集的处理
- StatementHandler (prepare, parameterize, batch, update, query) //拦截SQL语法构建的处理
我们可以先看一下官网的例子,要想使用插件,有三步:
1.实现 Interceptor 接口
2.指定想要拦截的方法签名
3.在mybatis-config.xml的<plugins>标签里进行配置
前面分析源码的时候我们就看到了使用插件来代理Executor,最后调用了Interceptor接口的plugin方法来创建代理对象
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object 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 (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
public Object pluginAll(Object target) {
Interceptor interceptor;
for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
interceptor = (Interceptor)var2.next();
}
return target;
}
至于其他几个对象则是在具体的Executor执行doUpdate/doQuery方法的时候在创建对象之后创建了代理对象
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
int var6;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var6 = handler.update(stmt);
} finally {
this.closeStatement(stmt);
}
return var6;
}
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);
StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
//configuration.newStatementHandler最后调用了BaseStatementHandler的构造方法,这里创建了ParameterHandler和ResultSetHandler,并在创建后使用责任链模式创建代理对象
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
this.objectFactory = this.configuration.getObjectFactory();
if (boundSql == null) {
this.generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}
this.boundSql = boundSql;
this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql);
}
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler)this.interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
分析下MyBatis关于插件的几个核心类
Plugin
MyBatis里提供了一个Plugin 类,可以通过Plugin.wrap(target,inteceptor)方法来直接返回一个代理对象,mybatis的分页插件PageInterceptor也是直接调用了Plugin的wrap方法通过jdk动态代理创建代理对象,其用来增强的InvocationHandler就是Plugin
所以代理对象在执行方法的时候会执行下面的invoke代码,实际会调用Interceptor的intercept方法,因为这里没有目标对象方法的调用,所以我们在实现Interceptor的intercept方法时,需要自己调用目标方法
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor)
//获取目标对象的Class
Class<?> type = target.getClass();
//获取目标对象的接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//创建代理对象,满足注解的签名的实现类才会创建代理对象
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取需要拦截的方法
Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
} catch (Exception var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
Invocation
invocation里保存了目标对象和代理的方法,可以通过调用proceed()方法来调用目标对象的方法
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
...
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return this.method.invoke(this.target, this.args);
}
}
总结一下:MyBatis插件相关的类
对象 | 作用 |
Interceptor | 自定义插件需要实现的接口 |
InterceptChain | 配置文件中配置的插件会解析后会保存在 Configuration 的 InterceptChain 中 |
Plugin | 用来创建代理对象,包装四大对象 |
Invocation | 对被代理对象进行包装,可以调用 proceed()调用到被拦截的方法 |
public interface Interceptor {
//覆盖被拦截对象的原有方法(需要在实现里调用目标对象的方法)
Object intercept(Invocation var1) throws Throwable;
//创建代理对象
Object plugin(Object var1);
//用于设置在mybatis-config.xml里配置的property属性
void setProperties(Properties var1);
}
PageHelper原理
用法
PageHelper.startPage(pageNumber, pageSize); //pageNumber, pageSize,第几页,每页几条
List<?> list= service.getAll();
PageInfo page = new PageInfo(list, 10);
分页插件的核心类PageInterceptor,从实现类的注解可以看到,拦截的方法是Executor的两个重载的query方法
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public Object intercept(Invocation invocation) throws Throwable {
try {
//获取目标对象的参数,根据拦截的方法签名获取各个参数
Object[] args = invocation.getArgs();
//获得MappedStatement参数,里面封装了跟本次statement id相关的各种参数
MappedStatement ms = (MappedStatement)args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds)args[2];
ResultHandler resultHandler = (ResultHandler)args[3];
//通过invocation的getTarget方法获取被代理的Executor对象,后面直接调用而不是调用
//invocation.proceed()来执行目标对象的方法
Executor executor = (Executor)invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
cacheKey = (CacheKey)args[4];
boundSql = (BoundSql)args[5];
}
List resultList;
if (this.dialect.skip(ms, parameter, rowBounds)) {
//执行目标对象的方法
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
} else {
Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
countKey.update("_Count");
MappedStatement countMs = (MappedStatement)this.msCountMap.get(countKey);
if (countMs == null) {
countMs = MSUtils.newCountMappedStatement(ms);
this.msCountMap.put(countKey, countMs);
}
String countSql = this.dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
Iterator var16 = additionalParameters.keySet().iterator();
while(var16.hasNext()) {
String key = (String)var16.next();
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long)((List)countResultList).get(0);
if (!this.dialect.afterCount(count, parameter, rowBounds)) {
Object var18 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
return var18;
}
}
if (!this.dialect.beforePage(ms, parameter, rowBounds)) {
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
} else {
parameter = this.dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
//调用dialect的getPageSql方法获取分页SQL
String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
Iterator var25 = additionalParameters.keySet().iterator();
while(true) {
if (!var25.hasNext()) {
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
break;
}
String key = (String)var25.next();
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
}
}
Object var22 = this.dialect.afterPage(resultList, parameter, rowBounds);
return var22;
} finally {
this.dialect.afterAll();
}
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//AbstractHelperDialect
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
Page page = this.getLocalPage();
return this.getPageSql(sql, page, pageKey);
}
最后会调用AbstractHelperDialect的getPageSql抽象方法,根据不同的数据库选择不同的实现类
public class MySqlDialect extends AbstractHelperDialect {
public MySqlDialect() {
}
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getPageSize());
} else {
//从Page对象获取对应的分页信息 起始下标和获取个数拼接SQL
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getStartRow());
sqlBuilder.append(",");
sqlBuilder.append(page.getPageSize());
pageKey.update(page.getStartRow());
}
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}
}
这里的Page是通过getLocalPage方法获取的,实际是从PageMethod里的ThreadLocal里获取的,所以我们在使用的时候直接设置page信息就可以了,因为ThreadLocal帮我们保证了线程安全
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
public PageMethod() {
}
//PageHelper.startPage最终调用的方法
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
public static <T> Page<T> getLocalPage() {
return (Page)LOCAL_PAGE.get();
}
应用场景
1.分表
在接口上添加注解,然后使用Interceptor对 query,update 方法进行拦截 ,根据注解上配置的参数进行分表操作
2.数据加解密
可以拦截获得入参和返回值,在update的时候加密;查询的时候解密
3. 菜单权限控制
对 query 方法进行拦截,在方法上添加不同的权限注解,这样就可以注解的权限信息,在 SQL 上加上权限过滤条件