Mybatis如何加载配置文件 源码解读parameterType


 

我能学到什么

--------------------------------------------------------------------------------------------------------------------------------------------------

1.        Mybatis加载解析配置文件流程

2.        如何解析配置文件里面的parameterType

3.        提高看源码的能力

4.        查看源码编写方式,明白应该如何规范的写出解析XML文件的代码,提高编码能力

5.        学会使用框架上解决问题的思维和常用手段

    • l        利用配置文件解耦
    • l        利用反射、动态代理、泛型、设计模式等解决框架级别的问题。
    • l        框架的设计思想

 ----------------------------------------------------------------------------------------------------------------------------------------------------


题外问题

如下一段代码:调用了Mybatis提供的加载及解析配置文件功能。

public class DBAccess {
    public SqlSession getSqlSession() throws IOException
    {
        //1、通过配置文件获取数据库连接相关信息
        Readerreader=Resources.getResourceAsReader("hdu/terence/config/Configuration.xml");
        //2、通过配置信息构建SqlSessionFactory
        SqlSessionFactorySSF=new SqlSessionFactoryBuilder().build(reader);
        //3、通过SqlSessionFactory打开数据库会话
        SqlSessionsqlSession=SSF.openSession();
        return sqlSession;
    }
}

题外话

上述代码在Mybatis中的使用存在两个问题:

问题一:每次访问数据调用Sql语句的时候,都会临时的去调用加载配置文件解析,很耗性能。

问题二:另外,每次访问,都需要反复加载,耗费时间。

这个问题的暂且说一下解决办法:

针对第一个配置文件加载的时机问题,要自己写一个监听器,容器在启动的时候加载配置文件。

 【加载时机】-->【监听器】

针对第二个问题,通过单利模式存放监听器加载的配置内容,防止其重复加载。

 【重复加载】-->【单例模式】

在实际开发中则是通过Spring+Mybatis解决上述两个问题的。

 

 

源码解读

加载上篇

先说一下上述加载解析配置文件的代码:根据注释可知,此部分分为三步。

1.     通过配置文件获取数据库连接相关信息

2.     通过配置信息构建SqlSessionFactory

3.        通过SqlSessionFactory打开数据库会话

其中,在第二步通过build()方法进入Mybatis当中。

上述build()方法是在SqlSessionFactoryBuilder.class这个类中实现的:

public SqlSessionFactorybuild(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 previouserror.
      }
    }
  } 

看此句:

XMLConfigBuilderparser = new XMLConfigBuilder(reader, environment, properties)表示将转换后的reader、配置环境以及配置的各个属性包装在parser解析项中,然后通过return build(parser.parse())返回一个会话工厂,仍然需要进入另外的源码XMLConfigBilder.class中,找到parser()解析方法:

  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  } 
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only beused once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

     在构造函数当中,将parsed=flase,在configuration parse()方法中,先判别parsed是否为ture,如果是True,则表示已经加载解析过,抛出异常(防止重复加载,耗费性能耗费时间,防止出现类似于调用sql语句时候每次都调用DBAccesss一样出现的两个问题),否则将其赋值为true,然后继续解析加载文件,通过parser.evalNode("/configuration")进入XPathParser.Class中,通过里面的Document文件读取对象来解析文件(那么由此可以说明,Mybatis解析xml文件使用的是Dom对象和Java JDK中的类进行的)。

XPathParser.Class文件相关内容:

构造函数:

public XPathParser(String xml) {
    commonConstructor(false, null, null);
    this.document = createDocument(new InputSource(new StringReader(xml)));
  }
public XPathParser(Reader reader) {
    commonConstructor(false, null, null);
    this.document = createDocument(new InputSource(reader));
  }

看第二个构造函数可知此步表示reader对象的转化为输入流,然后转化为document对象。

 

配置文件

上述的源码目的是为了进入配置文件Configuration.xml文件解析,下面先贴出来配置文件

贴总配置文件Configuration.xml:

<?xml version="1.0"encoding="UTF-8" ?>
<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置声明拦截器,可在拦截器中获取该配置中的属性值,用作其他用途 -->
 <plugins>
     <plugin interceptor="hdu.terence.interceptor.PageInterceptor">
      <property name="test"value="123"/>
     </plugin>
</plugins>
 
  <environments default="development">
    <environment id="development">     
      <transactionManager type="JDBC">
        <property name="" value=""/>
      </transactionManager>     
      <dataSource type="UNPOOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <!--url连接,注意编码方式的指定-->
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/micromessage?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull"/>     
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>     
    </environment>
  </environments>
 
<!--映射配置文件,多个配置文件可以写多个mapper映射-->
  <mappers>
    <mapper resource="hdu/terence/config/sqlxml/Message.xml"/>
    <mapper resource="hdu/terence/config/sqlxml/Command.xml"/>
    <mapper resource="hdu/terence/config/sqlxml/CommandContent.xml"/>
  </mappers> 
</configuration> 

子配置文件Dao.xml---- Message.xml

<?xml version="1.0"encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
<mapper namespace="hdu.terence.dao.IMessage">
 
  <resultMap type="hdu.terence.bean.Message" id="MessageResult"> 
    <!--存放Dao值--> <!--type是和数据库对应的bean类名Message-->
    <id column="id" jdbcType="INTEGER"property="id"/> <!--主键标签-->
    <result column="COMMAND" jdbcType="VARCHAR"property="command"/>
    <result column="DESCRIPTION" jdbcType="VARCHAR" property="description"/>
    <result column="CONTENT" jdbcType="VARCHAR"property="content"/>
  </resultMap>
 
  <select id="queryMessageList" parameterType="java.util.Map" resultMap="MessageResult">
    select <include refid="columns"/> from MESSAGE
    <where>
    <if test="message.command != null and!"".equals(message.command.trim())">
        andCOMMAND=#{message.command}
        </if>
        <if test="message.description != null and!"".equals(message.description.trim())">
        andDESCRIPTION like '%' #{message.description} '%'
        </if>
    </where>
    order by ID limit#{page.dbIndex},#{page.dbNumber}
  </select> 
</mapper>

 

梳理总流程

OK,退出来梳理总流程:

    首先,建立一个总配置文件的解析流,使用Document对象代替reader对象,成为了新的配置文件解析代言人,然后Document对象进入Configuration.xml配置文件,解析出<mappers>……</mappers>的配置项,找到Dao.xml配置文件的路径,根据该路径,进入该配置文件(Message.xml)。

   最后,进入Dao.xml配置文件之后,使用JDK中的Dom对象逐个解析,肯定能找到<select>标签,然后找到该标签后面的属性parameterTypes参数类型属性,最后通过反射机制,利用参数类型获取参数类名。

 

加载下篇

下面就解读一下如何查询总配置文件和在总配置文件进入到sql语句Dao.xml配置文件:

先说一个东西:XPath

Xpath是XML的路径语言,使用路径表达式来表示配置文档中的结点、结点集:

XMLConfigBuilder.class:
  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only beused once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

这个方法就是利用Xpath的XML路径语言(也是一种路径表达式),获取的是总配置文件Configuration.xml文件的<configuration>这个根结点:

进入到XPathParser.class方法:

public XNode evalNode(String expression) {
return evalNode(document, expression);
 //document是解读对象,expresssion是路径表达式
  }
public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
      return null;
    }
    return new XNode(this, node, variables);
  }
public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
      return null;
    }
    return new XNode(this, node, variables);
  }
 
  private Object evaluate(String expression, Object root, QName returnType) {
    try {
      return xpath.evaluate(expression, root, returnType);
          //在此步就给出了要取出的参数的类型。
    } catch (Exception e) {
      throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
  } 

       主线里面有辅线,一层一层如同递归一样调用,将获取的对象保存下来,然后使用XMLConfigBuilder.class文件下的parseConfigation(XNode root)方法来解析内容。

private void parseConfiguration(XNode root) {
    try {
      Properties settings = settingsAsPropertiess(root.evalNode("settings"));
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      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 andobjectWrapperFactory 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 MapperConfiguration. Cause: " + e, e);
    }
  }

       此方法同样是利用路径表达式(上述代码中括号中引号里面的内容)遍历root根节点下层的结点,比如在总配置文件configuration.xml文件plugins用来配置插件或拦截器,environments用来配置数据库访问的各个属性,mappers用来配置sql语句文件dao.xml的映射路径。

      同样的,evalNode()方法就是上述用到的查找根节点的方法。

      看分支语句:mapperElement(root.evalNode("mappers"))通过调用mapperElement()入sql配置文件Dao.xml解析文件。 

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);
            XMLMapperBuildermapperParser = 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);
            XMLMapperBuildermapperParser = 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 aurl, resource or class, but not more than one.");
          }
        }
      }
    }
  }

该方法首先判断父节点是否为空,若不为空,则进入子节点<mapper>:

     如果存在包文件”package”,则进入configuration.addMappers(mapperPackage)分支;

     否则利用getStringAttribute()方法获取所有的属性名称和属性值;

     若仅Resource不为空,则将该节点暂存于线程池:        (ErrorContext.instance().resource(resource))

     然后利用Resouces.getResourceAsStream(resource)字节流方法获取该节点的属性值:   resource=”hdu/terence/config/sqlxml/Message.xml”

     将参数保存到流中: XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream,configuration, url,configuration.getSqlFragments());

     然后调用mapperParser.parse()方法解析,此方法需要进入XMLMapperBuilder.java类中

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
  }

     过程和上述判断类似:首先判断是否对其解析过,如果解析过,抛出异常,否则继续解析文件。

     在parser()方法中利用parser.evalNode("/mapper")找到根节点<mapper>,然后利用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 beempty");
      }
      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. Cause:" + e, e);
    }
  } 

     由上述代码可知,必须要配置namespace属性值,否则会抛出异常:Mapper's namespace cannot beempty

    然后依次获取parameterMap/resultMap/sql等内容,然后调用buildStatementFromContext(context.evalNodes("select|insert|update|delete")),对这些不同类型的sql语句进行处理,追溯--------------到如下函数(中间曲折迂回太多了)。

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }
    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);
 
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
 
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
 
    // Include Fragments before parsing
    XMLIncludeTransformerincludeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
 
    // Parse selectKey after includes andremove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
   
    // Parse the SQL (pre: <selectKey>and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}

 

源码主要通过语句:

Class<?> parameterTypeClass= resolveClass(parameterType);

根据参数类型获取参数类名。


  获取参数类名又是一个艰辛的过程: 通过函数resolveClass(parameterType)来一层一层追溯目标到 TypeAliasRegistry下的resolveAlias(alias)方法

 TypeAliaiRegistry类:中成员和方法

private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>(); 
  public TypeAliasRegistry() {
    registerAlias("string", String.class); 
    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
   registerAlias("integer", Integer.class);
    registerAlias("double", Double.class);
    registerAlias("float", Float.class);
    registerAlias("boolean", Boolean.class);
 
    registerAlias("byte[]", Byte[].class);
    registerAlias("long[]", Long[].class);
    registerAlias("short[]", Short[].class);
    registerAlias("int[]", Integer[].class);
   registerAlias("integer[]", Integer[].class);
    registerAlias("double[]", Double[].class);
    registerAlias("float[]", Float[].class);
    registerAlias("boolean[]", Boolean[].class);
 
    registerAlias("_byte", byte.class);
    registerAlias("_long", long.class);
    registerAlias("_short", short.class);
    registerAlias("_int", int.class);
   registerAlias("_integer", int.class);
    registerAlias("_double", double.class);
    registerAlias("_float", float.class);
    registerAlias("_boolean", boolean.class);
 
    registerAlias("_byte[]", byte[].class);
    registerAlias("_long[]", long[].class);
    registerAlias("_short[]", short[].class);
    registerAlias("_int[]", int[].class);
    registerAlias("_integer[]", int[].class);
    registerAlias("_double[]", double[].class);
    registerAlias("_float[]", float[].class);
    registerAlias("_boolean[]", boolean[].class);
 
    registerAlias("date", Date.class);
    registerAlias("decimal", BigDecimal.class);
    registerAlias("bigdecimal", BigDecimal.class);
    registerAlias("biginteger", BigInteger.class);
    registerAlias("object", Object.class);
 
    registerAlias("date[]", Date[].class);
    registerAlias("decimal[]", BigDecimal[].class);
    registerAlias("bigdecimal[]", BigDecimal[].class);
    registerAlias("biginteger[]", BigInteger[].class);
    registerAlias("object[]", Object[].class);
 
    registerAlias("map", Map.class);
    registerAlias("hashmap", HashMap.class);
    registerAlias("list", List.class);
    registerAlias("arraylist", ArrayList.class);
    registerAlias("collection", Collection.class);
    registerAlias("iterator", Iterator.class);
 
    registerAlias("ResultSet", ResultSet.class);
  }
 
  public <T> Class<T> resolveAlias(String string) {
    try {
      if (string == null) {
        return null;
      }
      // issue #748
      String key = string.toLowerCase(Locale.ENGLISH);
      Class<T> value; //存放类名
      if (TYPE_ALIASES.containsKey(key)) {
        value = (Class<T>) TYPE_ALIASES.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);
    }
  } 

       如果参数类型string!=null,则先将参数类型字符串转换为小写key,然后判断Type_Aliases这个Map对象里面是否包含key,如果有,则获取对应的类名(此处获取的是java自定义好的类名,如String.classInteger.classByte[].classCollection.class等等),否则,直接利用反射获取类名(此处获取的类名是自定义的类名),然后将获得类名返回。

      其余方法不再细说,可自行追溯。

 

再梳理一遍

上述这条解读源码的主线是解析参数parameterType对应的类名:

    reader(总配置文件路径)—>变为configuration代言人—>解读Configuration.xml总配置文件—>

  找到<mappers>结点下的sqi子配置文件的路径—>进入子配置文件—>利用JDK中Dom对象查找节点—>

  找到<select>节点—>获取该节点的parameterType属性—>根据该属性值利用反射获取对应类名。

 

OK,完毕!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值