前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis源码解析(一)——MyBatis初始化过程解析

MyBatis源码解析(一)——MyBatis初始化过程解析

作者头像
大闲人柴毛毛
发布2018-03-12 10:32:39
8530
发布2018-03-12 10:32:39
举报
文章被收录于专栏:大闲人柴毛毛

1. 准备工作

为了看清楚MyBatis的整个初始化过程,先创建一个简单的Java项目,目录结构如下图所示:

1.1 Product 产品实体类

代码语言:javascript
复制
public class Product {
    private long id;
    private String productName;
    private String productContent;
    private String price;
    private int sort;
    private int falseSales;
    private long category_id;
    private byte type;
    private byte state;
    // PS:省略setter、getter函数
}

1.2 ProductMapper 产品持久化接口

代码语言:javascript
复制
public interface ProductMapper {
    /**
     * 查询所有的产品
     * @return
     */
    List<Product> selectProductList();
}

1.3 ProductMapper.xml 产品映射文件

代码语言:javascript
复制
<?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="team.njupt.mapper.ProductMapper">
    <select id="selectProductList" resultType="team.njupt.entity.Product">
        select * from product
    </select>
</mapper>

1.4 db.properties 数据库配置文件

代码语言:javascript
复制
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/waimai?useUnicode=true&characterEncoding=utf8
username=root
password=xxxxxx

1.5 mybatis.xml MyBatis的配置文件

代码语言:javascript
复制
<?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>
    <properties resource="db.properties">
        <!--<property name="username" value="dev_user"/>-->
        <!--<property name="password" value="F2Fa3!33TYyg"/>-->
    </properties>

    <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="team/njupt/mapper/ProductMapper.xml"/>
    </mappers>
</configuration>

1.6 Main 主函数

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) throws IOException {

        String resource = "mybatis.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
            List<Product> productList = productMapper.selectProductList();
            for (Product product : productList) {
                System.out.printf(product.toString());
            }
        } finally {
            sqlSession.close();
        }
    }
}

2. MyBatis初始化过程

2.1 获取配置文件

当系统初始化时,首先会读取配置文件,并将其解析成InputStream java String resource = "mybatis.xml"; InputStream inputStream = Resources.getResourceAsStream(resource);

2.2 创建SqlSessionFactoryBuilder对象

SqlSessionFactoryBuilder的名字中可以看出,SqlSessionFactoryBuilder是用来创建SqlSessionFactory对象的。 来看一下SqlSessionFactoryBuilder源码:

SqlSessionFactoryBuilder中只有一些重载的build函数,这些build函数的入参都是MyBatis配置文件的输入流,返回值都是SqlSessionFactory;由此可见,SqlSessionFactoryBuilder的作用很纯粹,就是用来通过配置文件创建SqlSessionFactory对象的。

2.3 SqlSessionFactory创建过程

下面具体来看一下,build函数是如何创建SqlSessionFactory对象的。

代码语言:javascript
复制
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
2.3.1 构造XMLConfigBuilder对象

build函数首先会构造一个XMLConfigBuilder对象,从名字上大致可以猜到,该对象是用来解析XML配置文件的。下面来看一下XMLConfigBuilder的体系结构。

  • XMLxxxBuilder是用来解析XML配置文件的,不同类型XMLxxxBuilder用来解析MyBatis配置文件的不同部位。比如:XMLConfigBuilder用来解析MyBatis的配置文件,XMLMapperBuilder用来解析MyBatis中的映射文件(如上文提到的ProductMapper.xml),XMLStatementBuilder用来解析映射文件中的SQL语句。
  • 这些XMLxxxBuilder都有一个共同的父类——BaseBuilder。这个父类维护了一个全局的Configuration对象,MyBatis的配置文件解析后就以Configuration对象的形式存储。
  • 当创建XMLConfigBuilder对象时,就会初始化Configuration对象,并且在初始化Configuration对象的时候,一些别名会被注册到ConfigurationtypeAliasRegistry容器中。 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() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); …… }

2.3.2 解析配置文件

当有了XMLConfigBuilder对象之后,接下来就可以用它来解析配置文件了。

代码语言:javascript
复制
private void parseConfiguration(XNode root) {
    try {
      // 解析<properties>节点
      propertiesElement(root.evalNode("properties"));
      // 解析<settings>节点
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      // 解析<typeAliases>节点
      typeAliasesElement(root.evalNode("typeAliases"));
      // 解析<plugins>节点
      pluginElement(root.evalNode("plugins"));
      // 解析<objectFactory>节点
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // 解析<reflectorFactory>节点
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // 解析<environments>节点
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析<mappers>节点
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

从上述代码中可以看到,XMLConfigBuilder会依次解析配置文件中的<properties>< settings >< environments>< typeAliases >< plugins >< mappers >等属性。下面介绍下几个重要属性的解析过程。

2.3.2.1 节点的解析过程
  • 节点的定义如下: <properties resource="org/mybatis/example/config.properties"> <property name="username" value="dev_user"/> <property name="password" value="F2Fa3!33TYyg"/> </properties>
  • 节点的解析过程: /** * @Param context <properties>节点 */ private void propertiesElement(XNode context) throws Exception { if (context != null) { // 获取<properties>节点的所有子节点 Properties defaults = context.getChildrenAsProperties(); // 获取<properties>节点上的resource属性 String resource = context.getStringAttribute("resource"); // 获取<properties>节点上的url属性 String url = context.getStringAttribute("url"); // resource和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) { // 获取resource属性值对应的properties文件中的键值对,并添加至defaults容器中 defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { // 获取url属性值对应的properties文件中的键值对,并添加至defaults容器中 defaults.putAll(Resources.getUrlAsProperties(url)); } // 获取configuration中原本的属性,并添加至defaults容器中 Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } parser.setVariables(defaults); // 将defaults容器添加至configuration中 configuration.setVariables(defaults); } }
    • 首先读取<resources>节点下的所有<resource>节点,并将每个节点的namevalue属性存入Properties中。
    • 然后读取<resources>节点上的resourceurl属性,并获取指定配置文件中的namevalue,也存入Properties中。(PS:由此可知,如果resource节点上定义的属性和properties文件中的属性重名,那么properties文件中的属性值会覆盖resource节点上定义的属性值。)
    • 最终,携带所有属性的Properties对象会被存储在Configuration对象中。
2.3.2.2 节点的解析过程
  • 节点的定义如下:
代码语言:javascript
复制
  <settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="multipleResultSetsEnabled" value="true"/>
  </settings>
  • 节点的解析过程: <settings>属性的解析过程和 <properties>属性的解析过程极为类似,这里不再赘述。最终,所有的setting属性都被存储在Configuration对象中。
2.3.2.3 属性的解析过程

<typeAliases>属性的定义方式有如下两种:

  • 方式1: <typeAliases> <typeAlias alias="Author" type="domain.blog.Author"/> <typeAlias alias="Blog" type="domain.blog.Blog"/> </typeAliases>
  • 方式2: <typeAliases> <package name="domain.blog"/> </typeAliases> 采用这种方式时,MyBatis会为指定包下的所有类起一个别名,该别名为首字母小写的类名。 <typeAliases>节点的解析过程如下: private void typeAliasesElement(XNode parent) { if (parent != null) { // 遍历<typeAliases>下的所有子节点 for (XNode child : parent.getChildren()) { // 若当前结点为<package> if ("package".equals(child.getName())) { // 获取<package>上的name属性(包名) String typeAliasPackage = child.getStringAttribute("name"); // 为该包下的所有类起个别名,并注册进configuration的typeAliasRegistry中 configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } // 如果当前结点为< typeAlias > else { // 获取alias和type属性 String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); // 注册进configuration的typeAliasRegistry中 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); } } } } }
    • 如果<typeAliases>节点下定义了<package>节点,那么MyBatis会给该包下的所有类起一个别名(以类名首字母小写作为别名)
    • 如果<typeAliases>节点下定义了<typeAlias>节点,那么MyBatis就会给指定的类起指定的别名。
    • 这些别名都会被存入configurationtypeAliasRegistry容器中。
2.3.2.4 节点的解析过程

<mappers>节点的定义方式有如下四种:

  • 方式1: <mappers> <package name="org.mybatis.builder"/> </mappers>
  • 方式2: <mappers> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> </mappers>
  • 方式3: <mappers> <mapper url="file:///var/mappers/AuthorMapper.xml"/> </mappers>
  • 方式4: <mappers> <mapper class="org.mybatis.builder.AuthorMapper"/> </mappers> <mappers>节点的解析过程如下: private void mapperElement(XNode parent) throws Exception { if (parent != null) { // 遍历<mappers>下所有子节点 for (XNode child : parent.getChildren()) { // 如果当前节点为<package> if ("package".equals(child.getName())) { // 获取<package>的name属性(该属性值为mapper class所在的包名) String mapperPackage = child.getStringAttribute("name"); // 将该包下的所有Mapper Class注册到configuration的mapperRegistry容器中 configuration.addMappers(mapperPackage); } // 如果当前节点为<mapper> else { // 依次获取resource、url、class属性 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // 解析resource属性(Mapper.xml文件的路径) if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); // 将Mapper.xml文件解析成输入流 InputStream inputStream = Resources.getResourceAsStream(resource); // 使用XMLMapperBuilder解析Mapper.xml,并将Mapper Class注册进configuration对象的mapperRegistry容器中 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } // 解析url属性(Mapper.xml文件的路径) 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(); } // 解析class属性(Mapper Class的全限定名) else if (resource == null && url == null && mapperClass != null) { // 将Mapper Class的权限定名转化成Class对象 Class<?> mapperInterface = Resources.classForName(mapperClass); // 注册进configuration对象的mapperRegistry容器中 configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
    • MyBatis会遍历<mappers>下所有的子节点,如果当前遍历到的节点是<package>,则MyBatis会将该包下的所有Mapper Class注册到configurationmapperRegistry容器中。
    • 如果当前节点为<mapper>,则会依次获取resource、url、class属性,解析映射文件,并将映射文件对应的Mapper Class注册到configurationmapperRegistry容器中。

其中,<mapper>节点的解析过程如下:

代码语言:javascript
复制
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
  • 在解析前,首先需要创建XMLMapperBuilder,创建过程如下: private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) { // 将configuration赋给BaseBuilder super(configuration); // 创建MapperBuilderAssistant对象(该对象为MapperBuilder的协助者) this.builderAssistant = new MapperBuilderAssistant(configuration, resource); this.parser = parser; this.sqlFragments = sqlFragments; this.resource = resource; }
    • 首先会初始化父类BaseBuilder,并将configuration赋给BaseBuilder;
    • 然后创建MapperBuilderAssistant对象,该对象为XMLMapperBuilder的协助者,用来协助XMLMapperBuilder完成一些解析映射文件的动作。
  • 当有了XMLMapperBuilder后,便可进入解析<mapper>的过程: public void parse() { // 若当前的Mapper.xml尚未被解析,则开始解析 // PS:若<mappers>节点下有相同的<mapper>节点,那么就无需再次解析了 if (!configuration.isResourceLoaded(resource)) { // 解析<mapper>节点 configurationElement(parser.evalNode("/mapper")); // 将该Mapper.xml添加至configuration的LoadedResource容器中,下回无需再解析 configuration.addLoadedResource(resource); // 将该Mapper.xml对应的Mapper Class注册进configuration的mapperRegistry容器中 bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
  • configurationElement函数 private void configurationElement(XNode context) { try { // 获取<mapper>节点上的namespace属性,该属性必须存在,表示当前映射文件对应的Mapper Class是谁 String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } // 将namespace属性值赋给builderAssistant builderAssistant.setCurrentNamespace(namespace); // 解析<cache-ref>节点 cacheRefElement(context.evalNode("cache-ref")); // 解析<cache>节点 cacheElement(context.evalNode("cache")); // 解析<parameterMap>节点 parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析<resultMap>节点 resultMapElements(context.evalNodes("/mapper/resultMap")); // 解析<sql>节点 sqlElement(context.evalNodes("/mapper/sql")); // 解析sql语句 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } }
  • resultMapElements函数 该函数用于解析映射文件中所有的<resultMap>节点,这些节点会被解析成ResultMap对象,存储在Configuration对象的resultMaps容器中。
    • <resultMap>节点定义如下: <resultMap id="userResultMap" type="User"> <constructor> <idArg column="id" javaType="int"/> <arg column="username" javaType="String"/> </constructor> <result property="username" column="user_name"/> <result property="password" column="hashed_password"/> </resultMap>
    • <resultMap>节点的解析过程: private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); // 获取<ResultMap>上的id属性 String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); // 获取<ResultMap>上的type属性(即resultMap的返回值类型) String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); // 获取extends属性 String extend = resultMapNode.getStringAttribute("extends"); // 获取autoMapping属性 Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); // 将resultMap的返回值类型转换成Class对象 Class<?> typeClass = resolveClass(type); Discriminator discriminator = null; // resultMappings用于存储<resultMap>下所有的子节点 List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); resultMappings.addAll(additionalResultMappings); // 获取并遍历<resultMap>下所有的子节点 List<XNode> resultChildren = resultMapNode.getChildren(); for (XNode resultChild : resultChildren) { // 若当前节点为<constructor>,则将它的子节点们添加到resultMappings中去 if ("constructor".equals(resultChild.getName())) { processConstructorElement(resultChild, typeClass, resultMappings); } // 若当前节点为<discriminator>,则进行条件判断,并将命中的子节点添加到resultMappings中去 else if ("discriminator".equals(resultChild.getName())) { discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); } // 若当前节点为<result>、<association>、<collection>,则将其添加到resultMappings中去 else { // PS:flags仅用于区分当前节点是否是<id>或<idArg>,因为这两个节点的属性名为name,而其他节点的属性名为property List<ResultFlag> flags = new ArrayList<ResultFlag>(); if ("id".equals(resultChild.getName())) { flags.add(ResultFlag.ID); } resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); } } // ResultMapResolver的作用是生成ResultMap对象,并将其加入到Configuration对象的resultMaps容器中(具体过程见下) ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); try { return resultMapResolver.resolve(); } catch (IncompleteElementException e) { configuration.addIncompleteResultMap(resultMapResolver); throw e; } }

    ResultMapResolver这个类很纯粹,有且仅有一个函数resolve,用于构造ResultMap对象,并将其存入Configuration对象的resultMaps容器中;而这个过程是借助于MapperBuilderAssistant.addResultMap完成的。 public ResultMap resolve() { return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping); }

  • sqlElement函数 该函数用于解析映射文件中所有的<sql>节点,并将这些节点存储在当前映射文件所对应的XMLMapperBuilder对象的sqlFragments容器中,供解析sql语句时使用。 <sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
  • buildStatementFromContext函数 该函数会将映射文件中的sql语句解析成MappedStatement对象,并存在configurationmappedStatements

2.3.3 创建SqlSessionFactory对象

代码语言:javascript
复制
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

回过头来再看一下SqlSessionFactorybuild函数,刚才说了半天,介绍了XMLConfigBuilder解析映射文件的过程,解析完成之后parser.parse()函数会返回一个包含了映射文件解析结果的configuration对象,紧接着,这个对象将作为参数传递给另一个build函数,如下:

代码语言:javascript
复制
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

这个函数将configuration作为参数,创建了DefaultSqlSessionFactory对象。 DefaultSqlSessionFactory是接口SqlSessionFactory的一个实现类,SqlSessionFactory的体系结构如下图所示:

此时,SqlSessionFactory创建完毕!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017年10月11日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 准备工作
    • 1.1 Product 产品实体类
      • 1.2 ProductMapper 产品持久化接口
        • 1.3 ProductMapper.xml 产品映射文件
          • 1.4 db.properties 数据库配置文件
            • 1.5 mybatis.xml MyBatis的配置文件
              • 1.6 Main 主函数
              • 2. MyBatis初始化过程
                • 2.1 获取配置文件
                  • 2.2 创建SqlSessionFactoryBuilder对象
                    • 2.3 SqlSessionFactory创建过程
                      • 2.3.2 解析配置文件
                      • 2.3.3 创建SqlSessionFactory对象
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档