Mybatis 在运行过程中,可以自己编写插件做一些全局处理。我们以一个插件为例子查看其原理。
1. 注入过程
1. 在构造会话工厂的时候注入拦截器
@Bean public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean(); /** * 重点,使分页插件生效 */ // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource作为数据源则不能实现切换 sessionFactory.setDataSource(dataSource); // 扫描Model sessionFactory.setTypeAliasesPackage("com.xm.ggn.bean"); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // 扫描映射文件 sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/**/*mapper.xml")); GlobalConfiguration defaults = GlobalConfigUtils.defaults(); // 设置下划线为false defaults.setDbColumnUnderline(false); // 设置自定义SQL注入器 defaults.setSqlInjector(new MyAutoSqlInjector()); sessionFactory.setGlobalConfig(defaults); // 添加插件 Interceptor[] interceptors = getPlugins(); if (ArrayUtils.isNotEmpty(interceptors)) { sessionFactory.setPlugins(interceptors); } return sessionFactory; } private Interceptor[] getPlugins() { Interceptor[] plugins = new Interceptor[0]; // PageHelper分页插件 PageInterceptor pageInterceptor = new PageInterceptor(); Properties properties = new Properties(); properties.setProperty("helperDialect", "mysql"); properties.setProperty("reasonable", "true"); pageInterceptor.setProperties(properties); SqlExplainInterceptor sqlExplainInterceptor = new SqlExplainInterceptor(); plugins = ArrayUtils.add(plugins, pageInterceptor); plugins = ArrayUtils.add(plugins, sqlExplainInterceptor); return plugins; }
2. com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean#buildSqlSessionFactory 构造过程中通过如下代码添加到Configuration 对象
if (!isEmpty(this.plugins)) { for (Interceptor plugin : this.plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'"); } } }
3. org.apache.ibatis.session.Configuration#addInterceptor 维护到自己内部属性中
protected final InterceptorChain interceptorChain = new InterceptorChain(); public void addInterceptor(Interceptor interceptor) { this.interceptorChain.addInterceptor(interceptor); }
这里采用了一个责任链条模式的设计:org.apache.ibatis.plugin.InterceptorChain 源码如下:
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; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
代码跟到这里发现维护到了内部链条的集合属性中。
2 调用过程
以com.baomidou.mybatisplus.plugins.SqlExplainInterceptor 为例子分析其执行过程。
1. 源码如下:
package com.baomidou.mybatisplus.plugins; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.Properties; import org.apache.ibatis.builder.StaticSqlSource; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.LogFactory; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.session.Configuration; import com.baomidou.mybatisplus.enums.DBType; import com.baomidou.mybatisplus.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.toolkit.GlobalConfigUtils; import com.baomidou.mybatisplus.toolkit.StringUtils; import com.baomidou.mybatisplus.toolkit.VersionUtils; /** * <p> * SQL 执行分析拦截器【 目前只支持 MYSQL-5.6.3 以上版本 】 * </p> * * @author hubin * @Date 2016-08-16 */ @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}) public class SqlExplainInterceptor implements Interceptor { private static final Log logger = LogFactory.getLog(SqlExplainInterceptor.class); /** * Mysql支持分析SQL的最小版本 */ private final String minMySQLVersion = "5.6.3"; /** * 发现执行全表 delete update 语句是否停止执行 */ private boolean stopProceed = false; @Override public Object intercept(Invocation invocation) throws Throwable { /** * 处理 DELETE UPDATE 语句 */ MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; if (ms.getSqlCommandType() == SqlCommandType.DELETE || ms.getSqlCommandType() == SqlCommandType.UPDATE) { Executor executor = (Executor) invocation.getTarget(); Configuration configuration = ms.getConfiguration(); Object parameter = invocation.getArgs()[1]; BoundSql boundSql = ms.getBoundSql(parameter); Connection connection = executor.getTransaction().getConnection(); String databaseVersion = connection.getMetaData().getDatabaseProductVersion(); if (GlobalConfigUtils.getDbType(configuration).equals(DBType.MYSQL) && VersionUtils.compare(minMySQLVersion, databaseVersion)) { logger.warn("Warn: Your mysql version needs to be greater than '5.6.3' to execute of Sql Explain!"); return invocation.proceed(); } /** * 执行 SQL 分析 */ sqlExplain(configuration, ms, boundSql, connection, parameter); } return invocation.proceed(); } /** * <p> * 判断是否执行 SQL * </p> * * @param configuration * @param mappedStatement * @param boundSql * @param connection * @param parameter * @return * @throws Exception */ protected void sqlExplain(Configuration configuration, MappedStatement mappedStatement, BoundSql boundSql, Connection connection, Object parameter) { StringBuilder explain = new StringBuilder("EXPLAIN "); explain.append(boundSql.getSql()); String sqlExplain = explain.toString(); StaticSqlSource sqlsource = new StaticSqlSource(configuration, sqlExplain, boundSql.getParameterMappings()); MappedStatement.Builder builder = new MappedStatement.Builder(configuration, "explain_sql", sqlsource, SqlCommandType.SELECT); builder.resultMaps(mappedStatement.getResultMaps()).resultSetType(mappedStatement.getResultSetType()) .statementType(mappedStatement.getStatementType()); MappedStatement queryStatement = builder.build(); DefaultParameterHandler handler = new DefaultParameterHandler(queryStatement, parameter, boundSql); try (PreparedStatement stmt = connection.prepareStatement(sqlExplain)) { handler.setParameters(stmt); try (ResultSet rs = stmt.executeQuery()) { while (rs.next()) { if (!"Using where".equals(rs.getString("Extra"))) { if (this.isStopProceed()) { throw new MybatisPlusException("Error: Full table operation is prohibited. SQL: " + boundSql.getSql()); } break; } } } } catch (Exception e) { throw new MybatisPlusException(e); } } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } return target; } @Override public void setProperties(Properties prop) { String stopProceed = prop.getProperty("stopProceed"); if (StringUtils.isNotEmpty(stopProceed)) { this.stopProceed = Boolean.valueOf(stopProceed); } } public boolean isStopProceed() { return stopProceed; } public void setStopProceed(boolean stopProceed) { this.stopProceed = stopProceed; } }
可以看到可以使用Intercepts 需要拦截的方法签名(包括所属类、方法名称、参数class)
2. 查看执行过程
查看Mybatis的Configuration 对象有下面四个方法:
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); 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); 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); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } public Executor newExecutor(Transaction transaction) { return newExecutor(transaction, defaultExecutorType); } 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; }
可以看到:
(1) 在构造ParameterHandler(java参数转为JDBC需要的参数处理器)、ResultSetHandler(负责将JDBC返回的ResultSet 结果集转换成List 类型的集合)、StatementHandler (封装JDBC Statement操作)、Executor( 执行器)过程中会调用interceptorChain.pluginAll
(2) interceptorChain.pluginAll 调用具体执行器的plugin方法,上面例子就是:com.baomidou.mybatisplus.plugins.SqlExplainInterceptor#plugin
方法内部先判断参数是否是Executor, 也就是这个拦截器只针对Executor 进行拦截。类注解上的方法签名也可以看出来
(3) 然后调用org.apache.ibatis.plugin.Plugin#wrap 进行包装, org.apache.ibatis.plugin.Plugin 源码如下:
package org.apache.ibatis.plugin; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.ibatis.reflection.ExceptionUtil; /** * @author Clinton Begin */ public class Plugin implements InvocationHandler { private final Object target; private final Interceptor interceptor; private final Map<Class<?>, Set<Method>> signatureMap; private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } 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; } @Override 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); } } private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); // issue #251 if (interceptsAnnotation == null) { throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); } }
org.apache.ibatis.plugin.Plugin#wrap 方法的核心逻辑是: 读取插件类上面的Intercepts 注解,然后获取Signature 上的注解,也就是读取拦截的相关类以及方法名称、参数类。最后通过反射读取到方法之后添加到signatureMap 属性中;然后用JDK创建代理对象之后返回。
例如:上面SqlExplainInterceptor 拦截器获取到的signatureMap 如下:
3. 这样通过Configuration 拿到的对象,如果需要走拦截器返回的就是代理对象;如果不需要拦截器使用的就是默认的相关对象。
比如上面使用了插件之后,返回的Executor实际是代理之后的对象;代理对象走的逻辑就是org.apache.ibatis.plugin.Plugin#invoke 方法里面逻辑。
4. Mybatis 执行过程中会先通过Configuration 上面四个newXXX 方法拿对象。拿到代理对象之后那么插件就会生效。这里也需要理解Mybais的执行过程。以一次查询为例分析其过程:
1》通过调用configuration.newExecutor(tx, execType); 创建Executor 执行器
2》执行查询或者修改最后会调用到XXXExecutor 的doUpdate或者doQuery 方法。比如:org.apache.ibatis.executor.SimpleExecutor#doQuery
@Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
可以看到首先调用configuration.newStatementHandler 创建StatementHandler, 这里会使用插件然后走代理机制。继续追代码会调用到:org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case CALLABLE: delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; default: throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); } }
然后会调用:org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler
public abstract class BaseStatementHandler implements StatementHandler { protected final Configuration configuration; protected final ObjectFactory objectFactory; protected final TypeHandlerRegistry typeHandlerRegistry; protected final ResultSetHandler resultSetHandler; protected final ParameterHandler parameterHandler; protected final Executor executor; protected final MappedStatement mappedStatement; protected final RowBounds rowBounds; protected BoundSql boundSql; 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 = configuration.getTypeHandlerRegistry(); this.objectFactory = configuration.getObjectFactory(); if (boundSql == null) { // issue #435, get the key before calculating the statement generateKeys(parameterObject); boundSql = mappedStatement.getBoundSql(parameterObject); } this.boundSql = boundSql; this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); } ...
也就是说创建StatementHandler的过程中会创建ParameterHandler、ResultSetHandler, 都是通过Configuration 创建的。
3》然后调用到org.apache.ibatis.executor.statement.SimpleStatementHandler#query
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { String sql = boundSql.getSql(); statement.execute(sql); return resultSetHandler.<E>handleResultSets(statement); }
这里的resultSetHandler就是上面的DefaultResultSetHandler(使用插件会先到代理)