1. 程式人生 > >Mybatis如何載入配置檔案 原始碼解讀parameterType

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,完畢!