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

来源:互联网 发布:全国省份数据库 编辑:程序博客网 时间:2024/06/06 01:54


 

我能学到什么

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

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

1 0