本系列主要分为三部分,前两部分主要分析mybatis的实现原理,最后一部分结合spring,来看看mybtais是如何与spring结合的就是就是mybatis-spring的源码。
相较于spring,mybatis源码算是比较容易理解的,因为很少用一层层的抽象,类所做的事一目了然,但是要说质量的话,我还是偏向与spring,只是个人意见,好了我们开始:
为了便于理解,我们分两部分介绍mybatis,本篇着重介绍mybtais是如何解析xml或者注解,并将其结构化为自己的类。
先看mybatis官网上的一个例子:
public static void main(String[] args) throws IOException { InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //本篇只分析到这 SqlSession sqlSession = sqlSessionFactory.openSession(); BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class); Blog blog = blogMapper.getById(1); }
抛开其他框架,我们只用mybatis的话可以看到,核心的几个类:
SqlSessionFactory,
SqlSession,
SqlSessionFactoryBuilder
老规矩,在正真看代码之前,我们先把核心的几个类拿出来解释一下,然后看一下类图,从整体上了解mybatis的设计:
SqlSessionFactory:顾名思义,sqlsession的工厂类,从上面的例子可以看出,一个SqlSessionFactory实例对应一个mybatis-config.xml配置即一个数据源
SqlSessionFactoryBuilder:SqlSessionFactory的组装车间,这里用了类似建造者模式,为啥是类似,因为这不是标准的建造者模式,这里说一句,mybatis里很多地方都用了这种不是很标准的建造者模式。
SqlSession:一次数据库会话对应一个SqlSession实例
Configuration:这个类是本篇的重点,它和sqlsessionfactory的重要产出,我们在xml或者注解中的几乎所有配置都会被解析并装载到configuration的实例中。
MappedStatement:这个类实际上是被configuration持有的,之所以拿出来单独说,是因为它太重要了,我们的sql相关的配置,都会被解析放在这个类的实例中,因此,很多分页插件也是通过改变这个类中的sql,来将分页的逻辑切入正常逻辑中。(当然不仅仅可以做分页,也可以做加密等等)。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
Configuration这个类的实例是在XMLConfigBuilder的构造方法中被创造出来的,我们重点来看解析的逻辑:
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; }
顺便提一句,mybatis解析xml用的是java中自带的解析器的,有关xml解析的知识这里不会细讲,有兴趣的同学可以去了解一下dom4j,dom,sax,jdom等的区别和优劣。
private void parseConfiguration(XNode root) { try { //issue #117 read properties first 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); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
这里我们主要看这几个方法,typeAliasesElement方法是注册类的别名,我们在xml中指定resultType或paramterType时会用到。这里使用了之前配置的VFS的实现类去装载指定package底下的类的字节码,然后通过反射获取类的信息。
settingsElement方法是将之前的配置赋值给configuration实例,简单的赋值,这里就不上源码了。
typeHandlerElement注册了类型处理器,同样,和typeAliasesElement方法类似,这个方法也可以去扫描指定包路径底下的类,并为这些类创建别名,默认使用的是Class.getSimpleName(),即类名称的缩写。
接下来会解析插件,然后实例化注册到Configuration中,等运行时动态代理目标类, 这部分我们会在下一章重点分析,这里不做介绍,然后会把所有的配置都设置到configuration中,后面的解析datasource和解析typehandler的逻辑这里就不分析了,都是简单的解析赋值操作,我们重点来看mapperElement方法:
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } 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()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
这里主要是两部分,上面是扫描包,根据注解生成对应的mappedStatement,第二部分是解析xml配置,根据xml配置来生成mappedstatement,我们先看第一部分,根据注解生成mappedstatemnet:
configuration会把工作委托给MapperRegistry去做,MapperRegistry会持有所有被解析的接口(运行时生成动态代理用),而最终解析的产物:mappedstatement依然会被configuration实例持有放在mappedStatements的map中:
public void addMappers(String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); for (Class<?> mapperClass : mapperSet) { addMapper(mapperClass); } }
这里同样是扫描指定包路径地下的所有类,并且根据filter(new ResolverUtil.IsA(superType)),挑选出满足条件的类,这里的条件是Object.class,所以包底下的所有类都会被装进来,接下来就是遍历这些类然后解析了:
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 { 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); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
我们看到,这里只解析所有接口,MapperRegistry所持有的是一个knownMappers,这里会有一个工厂类的实例MapperProxyFactory,这个类会在下一章介绍,会在生成接口的动态代理时被调用,我们继续往下看,接下来就是接口的解析工作了:
public void parse() { String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { loadXmlResource(); configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { parseStatement(method); } } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods(); }
在执行真正解析之前,mybatis又去load了一次xml文件,这是为了防止之前没有装在xml,保证一定是xml被解析完,再解析接口,
void parseStatement(Method method) { Class<?> parameterTypeClass = getParameterType(method); LanguageDriver languageDriver = getLanguageDriver(method); SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver); if (sqlSource != null) { Options options = method.getAnnotation(Options.class); final String mappedStatementId = type.getName() + "." + method.getName(); Integer fetchSize = null; Integer timeout = null; StatementType statementType = StatementType.PREPARED; ResultSetType resultSetType = ResultSetType.FORWARD_ONLY; SqlCommandType sqlCommandType = getSqlCommandType(method); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = !isSelect; boolean useCache = isSelect; KeyGenerator keyGenerator; String keyProperty = null; String keyColumn = null; if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) { // first check for SelectKey annotation - that overrides everything else SelectKey selectKey = method.getAnnotation(SelectKey.class); if (selectKey != null) { keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver); keyProperty = selectKey.keyProperty(); } else if (options == null) { keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } else { keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; keyProperty = options.keyProperty(); keyColumn = options.keyColumn(); } } else { keyGenerator = NoKeyGenerator.INSTANCE; } if (options != null) { if (FlushCachePolicy.TRUE.equals(options.flushCache())) { flushCache = true; } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) { flushCache = false; } useCache = options.useCache(); fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348 timeout = options.timeout() > -1 ? options.timeout() : null; statementType = options.statementType(); resultSetType = options.resultSetType(); } String resultMapId = null; ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class); if (resultMapAnnotation != null) { String[] resultMaps = resultMapAnnotation.value(); StringBuilder sb = new StringBuilder(); for (String resultMap : resultMaps) { if (sb.length() > 0) { sb.append(","); } sb.append(resultMap); } resultMapId = sb.toString(); } else if (isSelect) { resultMapId = parseResultMap(method); } assistant.addMappedStatement( mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize, timeout, // ParameterMapID null, parameterTypeClass, resultMapId, getReturnType(method), resultSetType, flushCache, useCache, // TODO gcode issue #577 false, keyGenerator, keyProperty, keyColumn, // DatabaseID null, languageDriver, // ResultSets options != null ? nullOrEmpty(options.resultSets()) : null); } }
这洋洋洒洒一堆,目的就是为了解析注解的配置,然后构建一个mappedstatement,我们只看核心逻辑:
首先,mybatis会根据注解生成一个sqlSource,这个接口是承载sql的实例,接口只有一个方法:getBoundSql,这个方法会根据传入的参数,将原来的sql解析,替换为能被数据库识别执行的sql,然后放入boundsql中,同样,这个方法是在运行时才被调用的。这里我们只看生成sqlsource的逻辑:
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) { try { Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method); Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method); if (sqlAnnotationType != null) { if (sqlProviderAnnotationType != null) { throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName()); } Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType); final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation); return buildSqlSourceFromStrings(strings, parameterType, languageDriver); } else if (sqlProviderAnnotationType != null) { Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType); return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method); } return null; } catch (Exception e) { throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e); } }
这里只有两种情况,普通的sql,和sqlprovider,我们来看核心方法:
@Override public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) { // issue #3 if (script.startsWith("<script>")) { XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver()); return createSqlSource(configuration, parser.evalNode("/script"), parameterType); } else { // issue #127 script = PropertyParser.parse(script, configuration.getVariables()); TextSqlNode textSqlNode = new TextSqlNode(script); if (textSqlNode.isDynamic()) { return new DynamicSqlSource(configuration, textSqlNode); } else { return new RawSqlSource(configuration, script, parameterType); } } }
首先判断是不是脚本,如果是脚本则走解析脚本的逻辑,如果不是,则判断是否是动态sql,判断的逻辑就是sql中是否含有 ”${}" 这样的关键字,如果有则是动态sql,如果不是,则是静态的。静态的话在构造方法中还有一段逻辑:
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> clazz = parameterType == null ? Object.class : parameterType; sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>()); }
这段的作用,是继续解析sql中的 “#{}” ,将其替换为 ? ,然后返回一个StaticSqlSource的实例。其实DynamicSqlSource最中也是转化为StaticSqlSource的,只不过它是在getBoundSql被调用的时候才做的,而且这里还会把"${}"替换为相应的参数:
public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql; }
所以我们拿到的BoudSql的实例实际上已经经过了解析。
解析完sqlsource后,mybatis会生成相应的mappedstatement,为了区分不同的mappedstatement,mysql为其创建了一个Id:
final String mappedStatementId = type.getName() + "." + method.getName();
这里可以看到,Id是类名加上方法名,这里就有一个问题,当类中的方法被重载时,mybatis会认为有问题的,可以看到,虽然方法被重载,mappedStatementId依然是同一个,所以mybatis中sql的接口是不能重载的。
下面就是根据注解的配置,创建相应的对象,然后一起组装成mappedstatement对象,然后放入configuration实例中的mappedstatements中。
至此,mybaits的sql解析篇就到此结束了,当然,mybatis的功能还远远不止如此,我们将在下一篇,mybatis的执行中,看到mybatis在运行时是如何代理接口,mybatis的各种插件有事如何介入的。
转载请注明出处,谢谢~