这次呢,大致介绍一些Mybatis的实现原理与总体设计。
Mybatis提供了方便的方式,直接通过注入一个interface,就可以实现方便的数据库查询工作。
但是仔细观察会发现,每一个interface其实并没有自己的实现类,那么mybatis是怎么让他实际去读写数据库的呢?
其实就是通过 动态代理, 动态代理在Mybatis中用的很多。
而要讲解这个代理的流程,需要先说一下Mybatis的一个核心类
SqlSession本身是一个接口,结构并不复杂
<T> T selectOne(String statement, Object parameter); <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds); <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds); void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler); int insert(String statement, Object parameter); int update(String statement, Object parameter); int delete(String statement, Object parameter); void commit(boolean force); void rollback(boolean force); List<BatchResult> flushStatements(); void close(); void clearCache(); /** * Retrieves current configuration * @return Configuration */ Configuration getConfiguration(); /** * Retrieves a mapper. * @param <T> the mapper type * @param type Mapper interface class * @return a mapper bound to this SqlSession */ <T> T getMapper(Class<T> type); /** * Retrieves inner database connection * @return Connection */ Connection getConnection(); }
从接口就可以看出来,SqlSesion的核心功能,就是实际的数据库操作。
并且,中间有getMapper方法,也就是说,Mapper的代理Proxy其实是由SqlSession提供
而数据库操作需要的几个东西:数据库连接和数据库操作的语句,在它的接口中并没有体现出来。
而主要通过一个参数
int insert(String statement, Object parameter);]]></ac:plain-text-body>
一个statement来传递,这个所谓的statement,比较好理解,就是通过mapper.xml们中的配置,加载过来的东西。
mapper接口和mapper.xml的大致关系就是:
一个Mapper.xml的一个sql方法会通过xml解析成一个MappedStatment, 一个MappedStatement就是一整个sql方法的整合类,它的属性大概有
public final class MappedStatement { private String resource; private Configuration configuration; private String id; private Integer fetchSize; private Integer timeout; private StatementType statementType; private ResultSetType resultSetType; private SqlSource sqlSource; private Cache cache; private ParameterMap parameterMap; private List<ResultMap> resultMaps; private boolean flushCacheRequired; private boolean useCache; private boolean resultOrdered; private SqlCommandType sqlCommandType; private KeyGenerator keyGenerator; private String[] keyProperties; private String[] keyColumns; private boolean hasNestedResultMaps; private String databaseId; private Log statementLog; private LanguageDriver lang; private String[] resultSets; }
其id就是它的对应的接口方法的全名称
并且以Map的形式统一存在了Configuration的属性里面
然后,MapperProxy也就是实现接口动态代理的类
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) {...} }
这里我们可以看出,动态代理对接口的绑定。
而我们在代码中实际注入的,就是这个MapperProxy代理类
它的产生
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } ...... }
了解动态代理的同学能够明白,当执行被代理的interface的时候,如果执行的对象是一个代理对象,则就会运行到MapperProxy的invoke方法中。
而mapperMethod的execute方法当中,实际执行的是
public Object execute(SqlSession sqlSession, Object[] args) { Object result; if (SqlCommandType.INSERT == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); } else if (SqlCommandType.DELETE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); } else if (SqlCommandType.SELECT == command.getType()) { if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } } else { throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && ! method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }
MapperMethod采用 命令模式 运行,根据上下文的条件的不同,可以跳转到sqlSession对应不同的方法当中。
至此,我们应该都知道为何Myabtis只用Mapper接口就可以运行SQL了,因为mapper.xml文件中的命名空间,就对应的是interface的全路径,然后通过路径和方法,就可以将对应的Sql找到并运行。
从上面可以看到,映射器其实是通过动态代理,进入到了MapperMethod的execute方法,然后根据简单的判断,就进入到了SqlSession的增删改查的方法当中,但是这些方法具体是怎么执行的呢?
其实SqlSession下又四个核心的对象
Executor是真正执行Java和数据库交互的东西。Mybatis存在三种执行器,可以在文件中配置选择
分别是
可以看一下Mybatis如何创建Executor
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; }
其实这里就是根据哪种类型来创建一个新的Executor
每一个sqlSesiion都会创建一个全新的Executor
接下来以SimpleExecutor的查询方法为例
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); } } //prepare方法 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
可以简单的看出,configuration提供了StatementHandler的生产
然后通过调用StatementHandler的prepare方法来对进行一些预先的设置与编译
包括对数据库语句的预编译,防止SQL注入,以及一些超时时间,查询大小的设置等。
然后就进入到第二个重要对象,StatementHandler
这个是用来专门处理与数据库交互的。先看下Mybatis是怎么生成它的
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; }
实际创建的是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()); } }
可以看到,也有三种不同的Handler,并且作为代理存在于RoutingStatementHandler中。这三种不同的Handler其实也是对应着之前提到的三种不同的Executor
statment的执行就比较简单了
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { String sql = boundSql.getSql(); statement.execute(sql); return resultSetHandler.<E>handleResultSets(statement); }
简单的运行statment,然后将结果ResultSet交给Resulthandler去处理。
至此,我们可以看一下一整个SqlSession的查询过程的流程
我们之前有看到,四大对象在创建的时候,会调用一行代码
executor = (Executor) interceptorChain.pluginAll(executor);
这就是,将四大对象,与插件进行绑定。
这里使用了 责任链的设计模式
于是,我们可以无缝添加很多的插件在Mybatis的运行过程中,并且在四大对象调度的时候,寻找合适的时机运行我们的代码。这就是Mybatis的插件技术
Mybatis的插件是对Mybatis的底层的修改,所以是存在一定的危险性的
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
这里有三个方法
插件的代理用的是责任链模式,其就是一个对象,可以是Mybatis的Sqlsession的四大对象的任意一个,在多个角色中进行传递。在传递链条上任何一个插件都有可以处理它的权利。
以Executor为例子,前面说到过,创建的时候执行过
executor = (Executor) interceptorChain.pluginAll(executor);
pluginAll的实现是
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
比较好理解,只是将预先加载好的插件拿出来循环一次,然后依次调用其plugin方法,对新生成的executor进行代理设置。这里可以看出
一个target被代理一次之后,会被第二个插件进行再一次的代理,是一个递归的代理模式。
大致为:
生成代理的方式,Mybatis提供了一个现成的实现,可以直接调用
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); } }
可以看出,提供了一个Plugin类,然后调用其warp方法,就会对指定的对象生成一个动态代理对象。
而动态代理对象的方法执行的时候,就会自动跳转到invoke方法
而invoke方法就会调用其intercept方法,将一个包装好的Invocation对象作为参数传给它。
然后插件再对这个方法的执行与否,进行自己的判断与逻辑
PageHelper是我们在使用Mybatis中经常使用的,分页工具
在mybatis中的配置
<plugins> <!-- com.github.pagehelper为PageHelper类所在包名 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <!-- 4.0.0以后版本可以不设置该参数 --> <property name="dialect" value="mysql"/> <!-- 该参数默认为false --> <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --> <!-- 和startPage中的pageNum效果一样--> <property name="offsetAsPageNum" value="false"/> <!-- 该参数默认为false --> <!-- 设置为true时,使用RowBounds分页会进行count查询 --> <property name="rowBoundsWithCount" value="true"/> <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --> <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)--> <property name="pageSizeZero" value="true"/> <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --> <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 --> <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 --> <property name="reasonable" value="false"/> <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --> <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 --> <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 --> <!-- 不理解该含义的前提下,不要随便复制该配置 --> <property name="params" value="pageNum=pageHelperStart;pageSize=pageHelperRows;"/> <!-- 支持通过Mapper接口参数来传递分页参数 --> <property name="supportMethodsArguments" value="false"/> <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page --> <property name="returnPageInfo" value="none"/> </plugin> </plugins>
通过在mybatis-config.xml中配置之后,此插件PageHelper就会添加到Mybatis插件的责任链interceptorChain当中去。
对四大对象的加载过程中,就会依次生成对应的代理对象
public class PageHelper implements Interceptor { public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } public Object intercept(Invocation invocation) throws Throwable { if (autoRuntimeDialect) { SqlUtil sqlUtil = getSqlUtil(invocation); return sqlUtil.processPage(invocation); } else { if (autoDialect) { initSqlUtil(invocation); } return sqlUtil.processPage(invocation); } } }
从上面能看得出来,PageHelper其实是对整个Executor进行来代理,也就是说整个执行过程就行了责任处理。之后的流程细节就不仔细看了,不过原理明白了,也很容易联想到,之后是对sql的参数进行了拦截,然后添加上了分页、排序、limit等信息。