代码有些多,怕有的大兄弟耐不下心, 就先写一个总结。
mybatis的的大概流程是这样的:
通过解析配置文件分析mapper文件和接口,生成代理对象。根据配置文件,创建会话通过会话拿到代理对象通过代理对象,执行具体方法,将接口和sql关联,并执行。Note: mybatis version --3.4.6
解析配置文件,然后构建出SqlSessionFactory。build中有很多细节,需要一一分析出来。
从org.apache.ibatis.session.SqlSessionFactoryBuilder#build()方法一直点下, 进入终极buiild。在终极build中,有parse方法,顾名思义,那就是解析,那就要去看去解析什么。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); // 进入parse方法。 return build(parser.parse()); } .. }进入org.apache.ibatis.builder.xml.XMLConfigBuilder#parse方法, 可以看出来,是在解析的配置文件,根节点是configuration。
public Configuration parse() { ... // 具体解析 parseConfiguration(parser.evalNode("/configuration")); // 返回的是configration对象。 return configuration; }接下来,进入parseConfiguration,继续追踪细节。
private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); // 解析mapper文件,我们需要具体关注。因为关乎后面如何通过接口代理来调用方法。 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }我们通过节点名称可以看出来,在解析configuration文件中的每个节点。但里面有个我们需要特别关注,那就是mapperElement(root.evalNode("mappers"));,这个关乎后面如何和java代理关联起来。通过接口来调用方法。我们进入这个方法,查看具体细节。
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { ... } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 因为我配置是resource,所以进入的是此方法。 mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { configuration.addMapper(mapperInterface); } else { } } } }这个方法里面是根据配置文件,采用什么逻辑来解析,因为我配置的mapper-resources, 所以进入的是上面方法。没啥好逼逼的,直接点进去看看。
public void parse() { if (!configuration.isResourceLoaded(resource)) { // 解析mapper配置文件 configurationElement(parser.evalNode("/mapper")); // 防止重复解析 configuration.addLoadedResource(resource); // 关联mapper.xml对应的的mapper.java文件 bindMapperForNamespace(); } ... }来了,就是这, 就是这!!让我开始裸看代码出现迷糊的地方,没有找到解析配置文件的地方。大家注意,我们一步步来。先从configurationElement(parser.evalNode("/mapper"));看起来。
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); // 需要关注这个,这个就是解析sql的方法入口 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { } }我们通过解析的节点可以看出来,是在解析配置中的节点。例如:resultMap,sql等。 我们需要关注buildStatementFromContext(context.evalNodes("select|insert|update|delete"));,因为这个是具体解析sql的方法。并且透露一下,这个里面也是生成MappedStatement的地方。
private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { // look here. 具体执行的地方。 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }我把两个代码一起贴出来,这两段代码没什么好解释的,我们直接看statementParser.parseStatementNode();
public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); ... // MappedStatement builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); }代码比较多,删了一部分。不过我们还是可以看出来,就是在解析每一个sql片段,生成一个MappedStatement. 不过我们还是要点进addMappedStatement方法, 因为里面还有一段生成id的逻辑。
public MappedStatement addMappedStatement(...){ if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } // 处理ID, 把id转换成使用namespace+方法名。 id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); configuration.addMappedStatement(statement); return statement; }我们在这就可以清楚的看到,mapper.xml文件最后被解析成了MappedStatement文件,并且id是以namespace+方法名组成的。并且把这些MappedStatement放入到了全局的configuration类中。自此,我们看到MappedStatement细节全部处理完成。
回过头,我们接着看bindMapperForNamespace();方法,这个方法也比较重要。也解释了为什么明明是接口(Interface), 但是可以返回对象。Let’s go .
private void bindMapperForNamespace() { // 获取MappedStatement 的Namespace String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { // 通过namespace获取接口类型。这里也说明了,为什么我们在配置mapper.xml文件的时候, // namespace 要和接口的包名相同 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace); // 关键点,把mapper.java类型保存起来 configuration.addMapper(boundType); } } }核心点了,解析到Mapper.java后,调用了configuration.addMapper(boundType);,里面具体是什么样的呢?我们一路点下去,最后addMapper的具体代码如下.
public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 核心关键点,把类型和,和类型生成的MapperProxyFactory放入了map中保存了起来 knownMappers.put(type, new MapperProxyFactory<T>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 通过接口,解析,因为有的sql直接写在接口上面。这样的也会生成MappedStatement对象。 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }我们看到,核心是把类型,和代理工厂放入到了Map中保存了起来。接着解析接口上面的sql。至此。build代码解析完了,核心关键点都点了出来。但是还有一个小细节,我们看到解析完,返回的configuration对象。然后通过buid()方法,build出来的是DefaultSqlSessionFactory。
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }从openSession()方法点进去,直接看看是如何开启一个会话的。(上面说到了,生成的是DefaultSqlSessionFactory, 别进错了)
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 获取数据库信息,已经解析完。 final Environment environment = configuration.getEnvironment(); // 生成TransactionFactory final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 创建事务 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 创建执行器 final Executor executor = configuration.newExecutor(tx, execType); // 生成SqlSession return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }这个里面有生成SqlSession的具体不走,前面没啥好看的,就是从解析出来的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 { // 如果没有输入类型,则默认创建的是 SimpleExecutor executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { // 如果开启了二级缓存,则会在对执行器进行包装 executor = new CachingExecutor(executor); } // 拦截器连 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }我们可以看到,执行器有三种类型,分别是BatchExecutor,ReuseExecutor,SimpleExecutor(就不介绍三种执行器了)。默认采用的是SimpleExecutor。
我们看代码,发现生成DefaultSqlSession传入了Executor,在sqlsession中调用方法时,具体的执行逻辑是通过executor执行的。那一个构造函数和查询方法看一下。
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) { this.configuration = configuration; this.executor = executor; this.dirty = false; this.autoCommit = autoCommit; } public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); // 通过执行器来执行查询 Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds); registerCursor(cursor); return cursor; } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }通过SqlSession获取Mapper对象。 这个就得好好说道说道了。明明是接口,为什么可以返回对象?我们从代码中,一直点下去,直接发现代码。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 从最开始解析保存接口和代理工厂的map中,获取对象的MapperProxyFactory final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { // 创建实例代理事例,我们的好好瞅瞅 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }我们发现getMapper,最后返回的是代理对象,这也就是为什么明明是接口,但是可以返回对象。但是为了明白具体逻辑,我们还是要进去看看具体是如何实现的,以及如何是把MappedStatement和接口关联起来的。
public T newInstance(SqlSession sqlSession) { // 创建 MapperProxy , final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } public class MapperProxy<T> implements InvocationHandler, Serializable { // 这是一个InvocationHandler,代理对象具体执行的逻辑 .... @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 这个里面有核心,方法是如何和Mapper关联起来的 final MapperMethod mapperMethod = cachedMapperMethod(method); // 具体执行逻辑 return mapperMethod.execute(sqlSession, args); } }看到这, 我们应该都明白了,接口调用的方法,通过走代理,然后在代理对象中进行逻辑处理。 这个时候,我们就要进cachedMapperMethod(method);,这里是核心。
private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { // 构造MapperMethod 对象, 这个里面就是具体的关联逻辑。 mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; } // MapperMethod的构造函数 public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { // 关联逻辑 this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); }这段代码是构建MapperMethod, 关键的逻辑在构造函数里面。我们来看SqlCommand的构造函数
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); final Class<?> declaringClass = method.getDeclaringClass(); // 核心逻辑,顾名思义,解析mappedStatement.并且是通过Method. MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); ... }到这,我们就得就去瞅瞅了,是如何解析MappedStatement的,关联接口方法和MappedStatement.
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) { // statementId String statementId = mapperInterface.getName() + "." + methodName; if (configuration.hasStatement(statementId)) { // 通过statementId获取存入的mappedStatement return configuration.getMappedStatement(statementId); } else if (mapperInterface.equals(declaringClass)) { return null; } for (Class<?> superInterface : mapperInterface.getInterfaces()) { if (declaringClass.isAssignableFrom(superInterface)) { // 父类接口。逻辑一样 MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration); if (ms != null) { return ms; } } } return null; }看到这,就知道了,是通过方法的类名+方法名,组成statementId,然后去早已经解析的MappedStatement中,找到对应的MappedStatment,就关联起来啦。
执行方法。通过上面分析,我们已经知道是通过代理,找到对应的MappedStatement,然后执行具体的sql.
ooook, 现在已经搞完全部的流程啦,要自己在捋顺啦,钢巴得!
挂上[GitHub][https://github.com/fattyca1/mybatis-demo]代码地址,拉下来,在本地Debug起来。