Mybatis源碼之配置初始化

基礎(chǔ)示例

一個(gè)Mybatis的項(xiàng)目基本配置信息是寫在一個(gè)xml文件中,指定數(shù)據(jù)庫(kù)類型、數(shù)據(jù)源、事務(wù)等相關(guān)信息,如下

<?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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

程序執(zhí)行之初會(huì)解析配置文件,后續(xù)將根據(jù)配置的信息初始化數(shù)據(jù)連接池、Mapper等,下面根據(jù)源碼看看配置文件的解析。

下面代碼展示了根據(jù)配置文件讀取文件流,解析文件流并生成 configuration 并生成 SqlSessionFactory

// 配置文件的位置
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// SqlSessionFactoryBuilder 會(huì)根據(jù) 文件流信息解析成configuration
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

構(gòu)建 SqlSessionFactory

public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
}

新建 XMLConfigBuilder,XMLConfigBuilder將負(fù)責(zé)將 xml 配置文件解析成 Configuration 實(shí)例,供其他實(shí)例使用。下面重點(diǎn)介紹Configuration 實(shí)例解析過(guò)程。

創(chuàng)建XMLConfigBuilder

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
  this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

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;
}

調(diào)用構(gòu)造函數(shù)時(shí)會(huì)新建一個(gè) XPathParser,parser會(huì)解析配置文件的信息成 document;同時(shí)調(diào)用了父類的構(gòu)造函數(shù),傳入新建的一個(gè)Configuration,這個(gè)新建的 Configuration 就是 parse() 方法的返回值,后續(xù)對(duì)配置文件的解析也是將配置文件的數(shù)據(jù)放到此 configuration 中。

// XPathParser 的構(gòu)造函數(shù)
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
  commonConstructor(validation, variables, entityResolver);
  this.document = createDocument(new InputSource(inputStream));
}

XMLConfigBuilder初始化完成,調(diào)用 parse() 方法解析配置文件的 configuration 節(jié)點(diǎn),這是整個(gè)配置文件的解析,也就是官方XML配置說(shuō)明的實(shí)現(xiàn)部分。

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  // 拿到configuration節(jié)點(diǎn),調(diào)用parseConfiguration方法,將解析結(jié)果放在configuration變量中
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

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);
  }
}

propertiesElement(root.evalNode("properties"));

properties節(jié)點(diǎn)示例,一般數(shù)據(jù)庫(kù)的連接配置會(huì)單獨(dú)拿出來(lái),放在外部配置中,resource 或者 url屬性指定

<properties resource="config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // 讀取 properties 節(jié)點(diǎn)的子節(jié)點(diǎn),加載為 Properties
      Properties defaults = context.getChildrenAsProperties();
        // resource 和 url都是外部配置資源路徑
      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);
      }
        // 解析完畢,設(shè)置值
      parser.setVariables(defaults);
      configuration.setVariables(defaults);
    }
}
  1. 屬性放在configuration的variables中
  2. XPathParser 的 variables中,解析其他數(shù)據(jù)需要使用

settingsAsProperties(root.evalNode("settings"));

settings 節(jié)點(diǎn),這是 MyBatis 中極為重要的調(diào)整設(shè)置,它們會(huì)改變 MyBatis 的運(yùn)行時(shí)行為。實(shí)例 如下

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

settingsAsProperties 方法代碼

private Properties settingsAsProperties(XNode context) {
  if (context == null) {
    return new Properties();
  }
  Properties props = context.getChildrenAsProperties();
  // 檢查屬性是否在Configuration類中存在
  // Check that all settings are known to the configuration class
  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;
}

獲取屬性值,并返回,返回的數(shù)據(jù)在 settingsElement(settings); 方法中設(shè)置到configuration中

loadCustomVfs(settings);

指定VFS的實(shí)現(xiàn),自定義VFS的實(shí)現(xiàn)的類全限定名,以逗號(hào)分隔。(不知道是啥)

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);
      }
    }
  }
}

需要是 VFS 的子類,VFS類的注釋

提供一個(gè)非常簡(jiǎn)單的API,用于訪問(wèn)應(yīng)用程序服務(wù)器中的資源。

應(yīng)該是為了某種擴(kuò)展吧。

typeAliasesElement(root.evalNode("typeAliases"));

類型別名是為 Java 類型設(shè)置一個(gè)短的名字。它只和 XML 配置有關(guān),存在的意義僅在于用來(lái)減少類完全限定名的冗余。

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <!--也可以指定一個(gè)包名-->
  <package name="domain.blog"/>
</typeAliases>

typeAliasesElement 代碼如下

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) {
              // 這個(gè) typeAliasRegistry 就是 configuration 的 typeAliasRegistry屬性,在 BaseBuilder的構(gòu)造函數(shù)中賦值
            typeAliasRegistry.registerAlias(clazz);
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
          throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
        }
      }
    }
  }
}

幾個(gè)相關(guān)方法

public void registerAliases(String packageName){
    registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType){
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
    for(Class<?> type : typeSet){
        // Ignore inner classes and interfaces (including package-info.java)
        // Skip also inner classes. See issue #6
        if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
            registerAlias(type);
        }
    }
}

public void registerAlias(Class<?> type) {
    // 類名
    String alias = type.getSimpleName();
    Alias aliasAnnotation = type.getAnnotation(Alias.class);
    if (aliasAnnotation != null) {
        alias = aliasAnnotation.value();
    } 
    registerAlias(alias, type);
}

public void registerAlias(String alias, Class<?> value) {
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // 默認(rèn)全小寫
    // issue #748
    String key = alias.toLowerCase(Locale.ENGLISH);
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
        throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    TYPE_ALIASES.put(key, value);
}

  1. 設(shè)置包名的將會(huì)掃描包下的類,設(shè)置別名
  2. 沒(méi)設(shè)置 alias 會(huì)以類名為別名
  3. 別名全小寫
  4. TypeAliasRegistry 類的構(gòu)造函數(shù)已經(jīng)設(shè)置了一些基礎(chǔ)數(shù)據(jù)類型以及集合數(shù)組類型等

將短名字添加到configuration的 typeAliasRegistry 屬性中

pluginElement(root.evalNode("plugins"));

MyBatis 允許你在已映射語(yǔ)句執(zhí)行過(guò)程中的某一點(diǎn)進(jìn)行攔截調(diào)用。

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
        // 實(shí)例化
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        // 設(shè)置 properties
      interceptorInstance.setProperties(properties);
        // 添加到 interceptor 鏈中,@see InterceptorChain
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

objectFactoryElement(root.evalNode("objectFactory"));

MyBatis 每次創(chuàng)建結(jié)果對(duì)象的新實(shí)例時(shí),它都會(huì)使用一個(gè)對(duì)象工廠(ObjectFactory)實(shí)例來(lái)完成。 默認(rèn)的對(duì)象工廠需要做的僅僅是實(shí)例化目標(biāo)類,要么通過(guò)默認(rèn)構(gòu)造方法,要么在參數(shù)映射存在的時(shí)候通過(guò)參數(shù)構(gòu)造方法來(lái)實(shí)例化。 如果想覆蓋對(duì)象工廠的默認(rèn)行為,則可以通過(guò)創(chuàng)建自己的對(duì)象工廠來(lái)實(shí)現(xiàn)。 默認(rèn)聲明的是ObjectFactory objectFactory = new DefaultObjectFactory(); 。

private void objectFactoryElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    Properties properties = context.getChildrenAsProperties();
    ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
    factory.setProperties(properties);
    configuration.setObjectFactory(factory);
  }
}

objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

reflectorFactoryElement(root.evalNode("reflectorFactory"));

settingsElement(settings);

將 settings 節(jié)點(diǎn)配置的參數(shù)設(shè)置到 configuration 中

environmentsElement(root.evalNode("environments"));

MyBatis 可以配置成適應(yīng)多種環(huán)境,這種機(jī)制有助于將 SQL 映射應(yīng)用于多種數(shù)據(jù)庫(kù)之中, 現(xiàn)實(shí)情況下有多種理由需要這么做。例如,開(kāi)發(fā)、測(cè)試和生產(chǎn)環(huán)境需要有不同的配置;或者共享相同 Schema 的多個(gè)生產(chǎn)數(shù)據(jù)庫(kù), 想使用相同的 SQL 映射。許多類似的用例。

每個(gè)數(shù)據(jù)庫(kù)對(duì)應(yīng)一個(gè) SqlSessionFactory 實(shí)例

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
</environments>

注意這里的關(guān)鍵點(diǎn):

  • 默認(rèn)的環(huán)境 ID(比如:default="development")。在構(gòu)建session工廠的時(shí)候指定,如未指定則為默認(rèn)值:SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
  • 每個(gè) environment 元素定義的環(huán)境 ID(比如:id="development")。
  • 事務(wù)管理器的配置(比如:type="JDBC")。
  • 數(shù)據(jù)源的配置(比如:type="POOLED")。
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)) {
          // 事務(wù)管理器
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 數(shù)據(jù)源
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
          // builder 模式構(gòu)建 environment
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

內(nèi)置的數(shù)據(jù)源和事務(wù)管理器相關(guān)的類,在 Configuration 構(gòu)造函數(shù)中,添加到 typeAliasRegistry 中

  1. 事務(wù)管理器
  2. 數(shù)據(jù)源
  3. 根據(jù)事務(wù)管理器、數(shù)據(jù)源、id生成Environment實(shí)例,賦值到configuration的environment

databaseIdProviderElement(root.evalNode("databaseIdProvider"));

MyBatis 可以根據(jù)不同的數(shù)據(jù)庫(kù)廠商執(zhí)行不同的語(yǔ)句,這種多廠商的支持是基于映射語(yǔ)句中的 databaseId 屬性。 MyBatis 會(huì)加載不帶 databaseId 屬性和帶有匹配當(dāng)前數(shù)據(jù)庫(kù) databaseId 屬性的所有語(yǔ)句。 如果同時(shí)找到帶有 databaseId 和不帶 databaseId 的相同語(yǔ)句,則后者會(huì)被舍棄。 為支持多廠商特性只要像下面這樣在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:

<databaseIdProvider type="DB_VENDOR" />

typeHandlerElement(root.evalNode("typeHandlers"));

無(wú)論是 MyBatis 在預(yù)處理語(yǔ)句(PreparedStatement)中設(shè)置一個(gè)參數(shù)時(shí),還是從結(jié)果集中取出一個(gè)值時(shí), 都會(huì)用類型處理器將獲取的值以合適的方式轉(zhuǎn)換成 Java 類型。 你可以重寫類型處理器或創(chuàng)建你自己的類型處理器來(lái)處理不支持的或非標(biāo)準(zhǔn)的類型。 具體做法為:實(shí)現(xiàn) org.apache.ibatis.type.TypeHandler 接口, 或繼承一個(gè)很便利的類 org.apache.ibatis.type.BaseTypeHandler, 然后可以選擇性地將它映射到一個(gè) JDBC 類型。

配置文件聲明自定義handler

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers> 
private void typeHandlerElement(XNode parent) throws Exception {
  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);
          // 也就是說(shuō)配置不指定javaType只指定jdbcType是無(wú)效的
        if (javaTypeClass != null) {
          if (jdbcType == null) {
            typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
          } else {
            typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
          }
        } else {
          typeHandlerRegistry.register(typeHandlerClass);
        }
      }
    }
  }
}
// register 最后都會(huì)通過(guò)調(diào)用TypeHandlerRegistry 的 下面方法執(zhí)行 注冊(cè)
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<JdbcType, TypeHandler<?>>();
            TYPE_HANDLER_MAP.put(javaType, map);
        }
        map.put(jdbcType, handler);
    }
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

通過(guò)類型處理器的泛型,MyBatis 可以得知該類型處理器處理的 Java 類型,不過(guò)這種行為可以通過(guò)兩種方法改變:

  • 在類型處理器的配置元素(typeHandler element)上增加一個(gè) javaType 屬性(比如:javaType="String");
  • 在類型處理器的類上(TypeHandler class)增加一個(gè) @MappedTypes 注解來(lái)指定與其關(guān)聯(lián)的 Java 類型列表。 如果在 javaType 屬性中也同時(shí)指定,則注解方式將被忽略。

可以通過(guò)兩種方式來(lái)指定被關(guān)聯(lián)的 JDBC 類型:

  • 在類型處理器的配置元素上增加一個(gè) jdbcType 屬性(比如:jdbcType="VARCHAR");
  • 在類型處理器的類上(TypeHandler class)增加一個(gè) @MappedJdbcTypes 注解來(lái)指定與其關(guān)聯(lián)的 JDBC 類型列表。 如果在 jdbcType 屬性中也同時(shí)指定,則注解方式將被忽略。

handlers 添加到 configuration 的 typeHandlerRegistry 屬性中

mapperElement(root.evalNode("mappers"));

既然 MyBatis 的行為已經(jīng)由上述元素配置完了,我們現(xiàn)在就要定義 SQL 映射語(yǔ)句了。但是首先我們需要告訴 MyBatis 到哪里去找到這些語(yǔ)句。 Java 在自動(dòng)查找這方面沒(méi)有提供一個(gè)很好的方法,所以最佳的方式是告訴 MyBatis 到哪里去找映射文件。你可以使用相對(duì)于類路徑的資源引用, 或完全限定資源定位符(包括 file:/// 的 URL),或類名和包名等。例如:

<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <!-- 或者
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  類全名
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  包名
  <package name="org.mybatis.builder"/>
  -->
</mappers>

關(guān)于 mapper 的解析是一個(gè)比較大的工作,下一節(jié)講解。

總結(jié)

configuration解析還是比較簡(jiǎn)單的,主要將配置文件的屬性賦值到configuration中供后續(xù)使用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評(píng)論 19 139
  • 1. 簡(jiǎn)介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存儲(chǔ)過(guò)程以及高級(jí)映射的優(yōu)秀的...
    笨鳥(niǎo)慢飛閱讀 6,227評(píng)論 0 4
  • 1 引言# 本文主要講解JDBC怎么演變到Mybatis的漸變過(guò)程,重點(diǎn)講解了為什么要將JDBC封裝成Mybait...
    七寸知架構(gòu)閱讀 77,531評(píng)論 36 979
  • CSS和JS在網(wǎng)頁(yè)中的放置順序是怎樣的?css要放在js之前css一般放在 標(biāo)簽里面js最好放在 標(biāo)簽之前 解釋白...
    菲龍?zhí)诫?yún)閱讀 330評(píng)論 0 0
  • 熱情的六月已為小學(xué)添上了圓滿的句號(hào),九月金秋用涼爽的秋風(fēng)將我們送入初中,新的環(huán)境,新的伙伴,未知的挑戰(zhàn)…...
    痞子兔Daivd閱讀 552評(píng)論 1 3

友情鏈接更多精彩內(nèi)容