😀 这里写文章的前言: 一个简单的开头,简述这篇文章讨论的问题、目标、人物、背景是什么?并简述你给出的答案。
可以说说你的故事:阻碍、努力、结果成果,意外与转折。
📝 题记 对于配置文件的解析, 还是相对比较好理解的, 就是读取配置文件, 然后在代码需要的地方给使用到.
这里,可以扩展下, Spring / SpringBoot 等是怎么读取配置文件呢 ? 并且配置文件还是有 xml / properties/yaml 等格式的 , 其读取代码是怎么写的 ? 然后基于 阿波罗(携程开源) 的配置中心 , 其实现配置又是怎么实现的呢 ? 然后这里,看了 Mybatis 读取配置文件, 后续再出 Spring 配置文件的时候,如果二者读取配置进行对比, 你个人更倾向使用代码呢 ?
所以,这里就开启读取 Mybatis 是如何解析配置文件的操作.
配置文件 这里的配置文件解读,是根据 MyBatis官网来一步一步的解析阅读. 如果有官网没有涉及到的,发现了也会在后续加上去的. 解析多行代码, 才能理解 何为优秀.
标签一 : properties org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration —> propertiesElement(root.evalNode(“properties”)) 方法中来
private void propertiesElement (XNode context) throws Exception { if (context != null ) { Properties defaults = context.getChildrenAsProperties(); String resource = context.getStringAttribute("resource" ); String url = context.getStringAttribute("url" ); if (resource != null && url != null ) { throw new BuilderException ("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other." ); } if (resource != null ) { defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null ) { defaults.putAll(Resources.getUrlAsProperties(url)); } Properties vars = configuration.getVariables(); if (vars != null ) { defaults.putAll(vars); } parser.setVariables(defaults); configuration.setVariables(defaults); } } ----------------------------- Properties dbConfigProperties = new Properties (); dbConfigProperties.setProperty("jdbc.password" ,"GavinYang" ); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder ().build(mybatisInputStream,dbConfigProperties);
标签二 : settings 这是 MyBatis对 settings 的操作.
具体的 settings 中每项配置参考官网链接 : https://mybatis.org/mybatis-3/configuration.html#properties
Properties settings = settingsAsProperties(root.evalNode("settings" ));loadCustomVfs(settings); loadCustomLogImpl(settings);
settingsAsProperties 方法
可以看到, 该方法就是进行加载,转化为key/value键值对类型, 然后对其key检验是否在
Configuration 中都有 set 方法.
Notes : 为了验证下, 我们加上一个没有的标签, 可以看到下面的异常. 所以我们看到这种异常的时候,是可以去检查下是不是名字什么有问题.
Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: org.apache.ibatis.builder.BuilderException: The setting nnnnn is not known. Make sure you spelled it correctly (case sensitive).
private Properties settingsAsProperties (XNode context) { if (context == null ) { return new Properties (); } Properties props = context.getChildrenAsProperties(); MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); for (Object key : props.keySet()) { if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException ("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)." ); } } return props; }
loadCustomVfs(settings) 方法
该方法,主要就是读取 vfsImpl 对用的value,切割下,然后用 classForName 来获取 class,
最后赋值到 configuration 中去. 这里算是对 vfs 的一种自定义的扩展,虽然目前还不太清楚vfs具体作用.
private void loadCustomVfs (Properties props) throws ClassNotFoundException { String value = props.getProperty("vfsImpl" ); if (value != null ) { String[] clazzes = value.split("," ); for (String clazz : clazzes) { if (!clazz.isEmpty()) { @SuppressWarnings("unchecked") Class<? extends VFS > vfsImpl = (Class<? extends VFS >)Resources.classForName(clazz); configuration.setVfsImpl(vfsImpl); } } } }
loadCustomLogImpl(settings) 方法
private void loadCustomLogImpl (Properties props) { Class<? extends Log > logImpl = resolveClass(props.getProperty("logImpl" )); configuration.setLogImpl(logImpl); } ----------------------- org.apache.ibatis.type.TypeAliasRegistry#resolveAlias public <T> Class<T> resolveAlias (String string) { try { if (string == null ) { return null ; } String key = string.toLowerCase(Locale.ENGLISH); Class<T> value; if (typeAliases.containsKey(key)) { value = (Class<T>) typeAliases.get(key); } else { value = (Class<T>) Resources.classForName(string); } return value; } catch (ClassNotFoundException e) { throw new TypeException ("Could not resolve type alias '" + string + "'. Cause: " + e, e); } } ------------ public void setLogImpl (Class<? extends Log> logImpl) { if (logImpl != null ) { this .logImpl = logImpl; LogFactory.useCustomLogging(this .logImpl); } }
标签三 : 关于别名的配置. typeAliasesElement(root.evalNode("typeAliases" )); private void typeAliasesElement (XNode parent) { if (parent != null ) { for (XNode child : parent.getChildren()) { if ("package" .equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name" ); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { String alias = child.getStringAttribute("alias" ); String type = child.getStringAttribute("type" ); try { Class<?> clazz = Resources.classForName(type); if (alias == null ) { typeAliasRegistry.registerAlias(clazz); } else { typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException ("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } } ---------------- public void registerAliases (String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil <>(); resolverUtil.find(new ResolverUtil .IsA(superType), packageName); Set<Class<? extends Class <?>>> typeSet = resolverUtil.getClasses(); for (Class<?> type : typeSet) { if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { registerAlias(type); } } }
标签四:扩展 扩展的 demo 可以参考 MyBatis官网 : https://mybatis.org/mybatis-3/configuration.html
然后看 MyBatis 是如何将插件给利用上的呢 ?
首先在 mybatis-config.xml 中配置好我们自己定义的 plugin
这里以我配置了二个插件
<plugins> <plugin interceptor="com.iyang.mybatis.plugins.ExamplePlugin" > <property name="name" value="GavinYang" /> <property name="age" value="22" /> <property name="hobby" value="lwf" /> </plugin> <plugin interceptor="com.iyang.mybatis.plugins.QuerySqlPlugin" > <property name="name" value="GavinYang" /> </plugin> </plugins>
// 处理 plugin 的代码
private void pluginElement (XNode parent) throws Exception { if (parent != null ) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor" ); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } }
可以看到 MyBatis在加载plugin的时候,是利用了反射来new出一个对象来,并且注册到 typeAliasRegistry 中来. 这里主要是解析 plugin 的配置, 后面在执行sql的时候,都是如何使用到这些 plugin 的呢 ? 肯定是有一个从InterceptorChain中获取interceptors来,然后进行处理.
标签五 : < objectFactory > objectFactory 的处理方式是和 标签四相似的,只是最后在使用场景是有点不同的.
代码上的操作也是类似的.
private void objectFactoryElement (XNode context) throws Exception { if (context != null ) { String type = context.getStringAttribute("type" ); Properties properties = context.getChildrenAsProperties(); ObjectFactory factory = (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance(); factory.setProperties(properties); configuration.setObjectFactory(factory); } }
该标签在 MyBatis 官网是没有demo, 我是根据代码来顺藤摸瓜写的一个.
参考 : org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory 这个源码,来模仿写的一个.
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory" )); private void objectWrapperFactoryElement (XNode context) throws Exception { if (context != null ) { String type = context.getStringAttribute("type" ); ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance(); configuration.setObjectWrapperFactory(factory); } }
标签六 : < reflectorFactory > 处理方式和上面类似.
这里我们自己写一个 com.iyang.mybatis.factory.GavinReflectorFactory 来继承DefaultReflectorFactory,在无参数构造函数中打印下内容, 然后debug跟进.
private void reflectorFactoryElement (XNode context) throws Exception { if (context != null ) { String type = context.getStringAttribute("type" ); ReflectorFactory factory = (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance(); configuration.setReflectorFactory(factory); } }
标签七:environments environments 标签都是放入一些 db 的配置信息等.
<environments default ="development" > <environment id="development" > <!-- 事务 --> <transactionManager type="JDBC" /> <!-- DB 连接配置 --> <dataSource type="POOLED" > <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value = "${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> private void environmentsElement (XNode context) throws Exception { if (context != null ) { if (environment == null ) { environment = context.getStringAttribute("default" ); } for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id" ); if (isSpecifiedEnvironment(id)) { TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager" )); DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource" )); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder = new Environment .Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); configuration.setEnvironment(environmentBuilder.build()); } } } }
解析 environments ,利用 typeAliasRegistry 中已经注册好了的信息,然后根据名字缩写(比如JDBC)这种,来获取class对象, 用 反射来 new 一波对象出来,真是美滋滋. 接着就是解析 事务/JDBC连接配置信息等, 最后将信息保存到 DataSource 中来. 反手再来一波 链式编程 来new对象出来, 最后就是一个 Environment 对象出来,给set 到 configuration 中来.
标签八: handler 到这里,可以看到对xml的解析操作. 先解析 标签 的值出来,然后根据值进行分类处理或者根据自己的需求来进行处理.
private void typeHandlerElement (XNode parent) { if (parent != null ) { for (XNode child : parent.getChildren()) { if ("package" .equals(child.getName())) { String typeHandlerPackage = child.getStringAttribute("name" ); typeHandlerRegistry.register(typeHandlerPackage); } else { String javaTypeName = child.getStringAttribute("javaType" ); String jdbcTypeName = child.getStringAttribute("jdbcType" ); String handlerTypeName = child.getStringAttribute("handler" ); Class<?> javaTypeClass = resolveClass(javaTypeName); JdbcType jdbcType = resolveJdbcType(jdbcTypeName); Class<?> typeHandlerClass = resolveClass(handlerTypeName); if (javaTypeClass != null ) { if (jdbcType == null ) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } } }
标签九 该标签是对我们对应的对象,其sql语句存放的地址. 也就是里面放入的是于mapper接口对应的方法,查询的sql语句.
接下来看下 MyBatis 是对 mappers 标签的内容进行了说明解析和处理.
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." ); } } } } } ----------------------- public void parse () { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper" )); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } ----------------------------------- 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" )); buildStatementFromContext(context.evalNodes("select|insert|update|delete" )); } catch (Exception e) { throw new BuilderException ("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } private void buildStatementFromContext (List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder (configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
🤗 总结归纳 📎 参考文章
💡 有关文章的问题,欢迎您在底部评论区留言,一起交流~