前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis:缓存,延迟加载,注解应用

MyBatis:缓存,延迟加载,注解应用

作者头像
RendaZhang
发布2020-09-08 16:17:52
8560
发布2020-09-08 16:17:52
举报
文章被收录于专栏:RendaRenda

MyBatis 加载策略

什么是延迟加载?

实际开发过程中很多时候并不需要总是在加载用户信息时就一定要加载他的订单信息。此时就是我们所说的延迟加载。

在一对多中,当有一个用户,它有个100个订单;在查询用户时,用户下的订单应该是,什么时候用,什么时候查询;在查询订单时,订单所属的用户信息应该是随着订单一起查询出来。

延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。

  • 优点:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
  • 缺点:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。

一对多,多对多:通常情况下采用延迟加载。

一对一(多对一):通常情况下采用立即加载。

注意:延迟加载是基于嵌套查询来实现的。

实现
局部延迟加载

associationcollection 标签中都有一个 fetchType 属性,通过修改它的值,可以修改局部的加载策略。

OrderMapper.xml

<!--
        fetchType="lazy" : 延迟加载策略
        fetchType="eager": 立即加载策略
    -->
<resultMap id="orderMap2" type="com.renda.domain.Orders">
    <id property="id" column="id"/>
    <result property="ordertime" column="ordertime"/>
    <result property="total" column="total"/>
    <result property="uid" column="uid"/>
    <association property="user" javaType="com.renda.domain.User"
                 select="com.renda.mapper.UserMapper.findById" column="uid" fetchType="lazy"/>
</resultMap>

<select id="findAllWithUser2" resultMap="orderMap2">
    SELECT * FROM orders
</select>
设置触发延迟加载的方法

在配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,在调用当前对象的 equalsclonehashCodetoString 方法时也会触发关联对象的查询。

OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
List<Orders> allWithUser2 = orderMapper.findAllWithUser2();

for (Orders orders : allWithUser2) {
    // 因为 Orders 的 toString 没有开启延迟加载
    // 配置了延迟加载的关联对象 User 还是被打印出来
    System.out.println(orders);
}

可以在配置文件 sqlMapConfig.xml 中使用 lazyLoadTriggerMethods 配置项覆盖掉上面四个方法。

<settings>
    <!-- 所有 toString 方法都会触发延迟加载 -->
    <setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
全局延迟加载

在 MyBatis 的核心配置文件中可以使用 setting 标签修改全局的加载策略。

<settings>
    <!-- 开启全局延迟加载功能 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 所有 toString 方法都会触发延迟加载 -->
    <setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>

配置完全局延迟加载功能后,需要加载关联的对象就需要调用它的 toString 方法:

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> allWithOrder2 = userMapper.findAllWithOrder2();

for (User user : allWithOrder2) {
    System.out.println(user);
    // 需要用到用户关联的订单
    System.out.println(user.getOrdersList());
}

注意:局部的加载策略优先级高于全局的加载策略;

所以,在开启全局延迟加载后,为了实现订单能立即加载关联的用户信息,就可以在局部开启立即加载策略:

<!--
        fetchType="lazy" : 延迟加载策略
        fetchType="eager": 立即加载策略
    -->
<resultMap id="orderMap2" type="com.renda.domain.Orders">
    <id property="id" column="id"/>
    <result property="ordertime" column="ordertime"/>
    <result property="total" column="total"/>
    <result property="uid" column="uid"/>
    <association property="user" javaType="com.renda.domain.User"
                 select="com.renda.mapper.UserMapper.findById" column="uid" fetchType="eager"/>
</resultMap>

<select id="findAllWithUser2" resultMap="orderMap2">
    SELECT * FROM orders
</select>

MyBatis 缓存

为什么使用缓存?

当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来,保存在缓存中。当用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查询。减少网络连接和数据库查询带来的损耗,从而提高我们的查询效率,减少高并发访问带来的系统性能问题。

一句话概括:经常查询一些不经常发生变化的数据,使用缓存来提高查询效率。

像大多数的持久化框架一样,MyBatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。MyBatis 中缓存分为一级缓存,二级缓存。

一级缓存
介绍

一级缓存是 SqlSession 级别的缓存,是默认开启的。

所以在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用一个 Mapper 方法,往往只执行一次 SQL,因为使用 SqlSession 第一次查询后,MyBatis 会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession 都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。

验证

资源目录 resources 下增加 log4j.properties

### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=C:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### set log levels - for more verbose logging change 'info' to 'debug' ###

log4j.rootLogger=debug, stdout

pom.xml中导入 log4j 的依赖,从而可以查看底层调用 JDBC 的 log:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.7</version>
</dependency>

编写代码验证 MyBatis 中的一级缓存:

@Test
public void testOneCache() throws IOException {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 根据 id 查询用户信息
    // 第一次查询,查询的数据库
    User user1 = userMapper.findById();
    System.out.println(user1);

    // 第二次查询,查询的是一级缓存
    User user2 = userMapper.findById();
    System.out.println(user2);
}

可以发现,虽然在上面的代码中查询了两次,但最后只执行了一次数据库操作,这就是 MyBatis 提供的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询 id 为 1 的记录时,并没有发出 SQL 语句从数据库中查询数据,而是从一级缓存中查询。

分析

一级缓存是 SqlSession 范围的缓存,执行 SqlSession 的 C(增加)U(更新)D(删除)操作,或者调用 clearCache()commit()close() 方法,都会清空缓存。

  1. 第一次发起查询用户 id 为 41 的用户信息,先去找缓存中是否有 id 为 41 的用户信息,如果没有,从数据库查询用户信息。
  2. 得到用户信息,将用户信息存储到一级缓存中。
  3. 如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
  4. 第二次发起查询用户 id 为 41 的用户信息,先去找缓存中是否有 id 为 41 的用户信息,缓存中有,直接从缓存中获取用户信息。
清除

sqlSession.clearCache() 手动清空一级缓存:

@Test
public void testOneCache() throws IOException {
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 根据 id 查询用户信息
    // 第一次查询,查询的数据库
    User user1 = userMapper.findById();
    System.out.println(user1);

    // clearCache: 手动清空缓存
    sqlSession.clearCache();

    // 第二次查询,查询的是一级缓存
    User user2 = userMapper.findById();
    System.out.println(user2);
}

flushCache="true" 自动清空一级缓存:

<select id="findById" resultType="com.renda.domain.User" parameterType="int" flushCache="true">
    SELECT * FROM `user` WHERE id = #{id}
</select>
二级缓存
介绍

二级缓存是 namspace 级别(跨 sqlSession)的缓存,是默认不开启的。

实现二级缓存的时候,MyBatis 要求返回的 POJO 必须是可序列化的,也就是要求实现 Serializable 接口。

二级缓存的开启需要进行配置,配置方法很简单,只需要在映射 XML 文件配置 <cache/> 就可以开启二级缓存了。

验证
配置核心配置文件
<settings>
    ...

    <!--
            cacheEnabled 的取值默认为 true,所以这一步可以省略不配置。
            为 true 代表支持二级缓存;为 false 代表不支持二级缓存。
        -->
    <setting name="cacheEnabled" value="true"/>
</settings>
配置 UserMapper.xml 映射
<mapper namespace="com.renda.mapper.UserMapper">

    <!-- 当前映射开启二级缓存 -->
    <cache></cache>

    <!--
        根据 id 查询用户
        useCache="true" 代表当前这个 statement 是使用二级缓存
    -->
    <select id="findById" resultType="com.renda.domain.User" parameterType="int" useCache="true">
        SELECT * FROM `user` WHERE id = #{id}
    </select>

</mapper>
修改 User 实体
public class User implements Serializable {
    private static final long serialVersionUID = 7898016747305399302L;

    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    private List<Orders> ordersList;
    private List<Role> roleList;

    ...   
}
测试结果
@Test
public void testTwoCache() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    // 第一次查询
    User user = userMapper1.findById();
    System.out.println(user);
    // 只有执行 sqlSession.commit 或者 sqlSession.close,那么一级缓存中内容才会刷新到二级缓存
    sqlSession1.close();

    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    // 第二次查询
    User user2 = userMapper2.findById();
    System.out.println(user2);
    sqlSession2.close();
}
分析

二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。

  1. 映射语句文件中的所有 select 语句将会被缓存。
  2. 映射语句文件中的所有 insertupdatedelete 语句会刷新缓存。

注意问题:MyBatis 的二级缓存因为是 namespace 级别,某个 namespace 的增删改只会刷新它自己的缓存,会导致不同 namespace 缓存了别的 namespace 的旧值,所以在进行多表查询时会产生脏读问题。

小结
  1. MyBatis 的缓存,都不需要手动存储和获取数据,是 MyBatis 自动维护的。
  2. MyBatis 开启了二级缓存后,那么查询顺序:二级缓存 --> 一级缓存 --> 数据库。
  3. 注意:因为 MyBatis 的二级缓存会存在脏读问题,所以实际开发中会使用第三方的缓存技术 Redis 解决问题。

MyBatis 注解

MyBatis 常用注解

这几年来注解开发越来越流行,MyBatis 也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。我们先围绕一些基本的 CRUD 来学习,再学习复杂映射多表操作。

  • @Insert:实现新增,代替了
  • @Delete:实现删除,代替了
  • @Update:实现更新,代替了
  • @Select:实现查询,代替了
  • @Result:实现结果集封装,代替了
  • @Results:可以与@Result 一起使用,封装多个结果集,代替了
  • @One:实现一对一结果集封装,代替了 <association></association>
  • @Many:实现一对多结果集封装,代替了
MyBatis注解的增删改查
创建 `UserMapper` 接口
public interface UserMapper {

    /**
     * 查询用户
     */
    @Select("select * from user")
    List<User> findAll();

    /**
     * 添加用户
     */
    @Insert("insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})")
    void save(User user);

    /**
     * 更新用户
     */
    @Update("update user set username=#{username}, birthday=#{birthday}, sex=#{sex} where id = #{id}")
    void update(User user);

    /**
     * 删除用户
     */
    @Delete("delete from user where id = #{id}")
    void delete(Integer id);
}
编写核心配置文件

sqlMapConfig.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>

    <!-- 加载 properties 文件 -->
    <properties resource="jdbc.properties"></properties>


    <settings>
        <!-- 开启全局延迟加载功能 -->
        <setting name="lazyLoadingEnabled" value="false"/>
        <!-- 所有方法都会延迟加载 -->
        <setting name="lazyLoadTriggerMethods" value="toString()"/>
        <!--
            因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。
            为 true 代表开启二级缓存;为 false 代表不开启二级缓存。
        -->
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!-- 设置别名 -->
    <typeAliases>
        <package name="com.renda.domain"/>
    </typeAliases>

    <!-- environments: 运行环境 -->
    <environments default="development">
        <environment id="development">
                <!-- 当前的事务事务管理器是 JDBC -->
            <transactionManager type="JDBC"></transactionManager>
                <!-- 数据源信息 POOLED:使用 mybatis 的连接池 -->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 指定扫描包含映射关系的接口所在的包 -->
    <mappers>
        <!-- 扫描使用注解的 Mapper 类所在的包 -->
        <package name="com.renda.mapper"/>
    </mappers>

</configuration>
测试代码
public class MyBatisTest {

    private SqlSessionFactory sqlSessionFactory;
    private SqlSession sqlSession;

    @Before
    public void before() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        sqlSession = sqlSessionFactory.openSession();
    }

    @After
    public void after(){
        sqlSession.commit();
        sqlSession.close();
    }

    /*
        测试查询方法
    */
    @Test
    public void testSelect() {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> all = userMapper.findAll();

        for (User user : all) {
            System.out.println(user);
        }
    }

    /*
        测试添加方法
     */
    @Test
    public void testInsert(){
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User user = new User();
        user.setUsername("布莱尔");
        user.setSex("女");
        user.setBirthday(new Date());
        user.setAddress("江苏");

        mapper.save(user);
    }

    @Test
    public void testUpdate(){
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User user = new User();
        user.setUsername("柳岩");
        user.setBirthday(new Date());
        user.setSex("女");
        user.setId();

        mapper.update(user);
    }

    @Test
    public void testDelete(){
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        mapper.delete();
    }

}
使用注解实现复杂映射开发

在映射文件中通过配置 <resultMap><association><collection> 来实现复杂关系映射。

使用注解开发后,可以使用 @Results@Result@One@Many 注解组合完成复杂关系的配置。

@Results

  • 代替的是标签 该注解中可以使用单个 @Result 注解,也可也使用 @Result 聚合。
  • 使用格式 - @Results ({@Result () , @Result() })@Results (@Result () )

@Result

  • 代替了 标签和 标签
  • @Result 中属性介绍:column - 数据库的列名,property - 需要装配的属性名,one - 需要使用的 @One 注解 @Result (one=@One) ()many - 需要使用的 @Many 注解 @Result (many=@many) ()

@One (一对一):

  • 代替了 标签,是多表查询的关键,在注解中用来指定子查询返回单一对象。
  • @One 属性介绍:select - 指定用来多表查询的 SQL Mapper,使用格式 - @Result(column="", property="", one=@One(select=""))

@Many(一对多):

  • 代替了 标签,是多表查询的关键,在注解中用来指定子查询返回对象集合。
  • 使用格式 - @Result(property="", column="", many=@Many(select=""))
一对一查询
介绍

需求:查询一个订单,与此同时查询出该订单所属的用户

一对一查询语句
SELECT * FROM orders;
SELECT * FROM `user` WHERE id = #{ 订单的 uid };
代码实现

OrderMapper 接口

@Select("select * from orders")
@Results({
    @Result(property="id",column = "id",id = true),
    @Result(property="ordertime",column = "ordertime"),
    @Result(property="total",column = "total"),
    @Result(property="uid",column = "uid"),
    @Result(property="user",column = "uid", javaType = User.class,
            one = @One(select = "com.renda.mapper.UserMapper.findById",fetchType = FetchType.EAGER))
})
List<Orders> findAllWithUser();

UserMapper 接口

@Select("select * from user where id = #{uid}")
User findById(Integer uid);

测试代码

@Test
public void testOneToOne() {
    OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);

    List<Orders> allWithUser = mapper.findAllWithUser();

    for (Orders orders : allWithUser) {
        System.out.println(orders);
    }
}
一对多查询
介绍

需求:查询一个用户,与此同时查询出该用户具有的订单

一对多查询语句
SELECT * FROM `user`;
SELECT * FROM orders where uid = #{ 用户 id };
代码实现

UserMapper 接口

@Select("select * from user")
@Results({
    @Result(property="id",column="id",id=true),
    @Result(property="username",column="username"),
    @Result(property="birthday",column="birthday"),
    @Result(property="sex",column="sex"),
    @Result(property="address",column="address"),
    @Result(property="ordersList",column="id",javaType=List.class,
            many=@Many(select="com.renda.mapper.OrderMapper.findOrderByUid",fetchType=FetchType.LAZY))
})
List<User> findAllWithOrder();

OrderMapper 接口

@Select("select * from orders where uid = #{uid}")
List<Orders> findOrderByUid(Integer uid);

测试代码

@Test
public void testOneToMany(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> allWithOrder = mapper.findAllWithOrder();

    for (User user : allWithOrder) {
        System.out.println(user);
        System.out.println(user.getOrdersList());
    }
}
多对多查询
介绍

需求:查询所有用户,同时查询出该用户的所有角色

多对多查询语句
SELECT * FROM `user`;
SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON r.`id` = ur.`rid` WHERE ur.`uid` = #{ 用户 id };
代码实现

UserMapper 接口

@Select("select * from user")
@Results({
    @Result(property="id",column="id",id=true),
    @Result(property="username",column="username"),
    @Result(property="birthday",column="birthday"),
    @Result(property="sex",column="sex"),
    @Result(property="address",column="address"),
    @Result(property="roleList",column="id",javaType=List.class,
            many=@Many(select="com.renda.mapper.RoleMapper.findAllByUid")),
})
List<User> findAllWithRole();

RoleMapper 接口

@Select("SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON ur.roleid = r.id WHERE ur.userid = #{uid}")
List<Role> findAllByUid(Integer uid);

测试代码

@Test
public void testManyToMany(){
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    List<User> allWithRole = mapper.findAllWithRole();

    for (User user : allWithRole) {
        System.out.println(user);
        System.out.println(user.getRoleList());
    }
}
基于注解的二级缓存

配置 SqlMapConfig.xml 文件开启二级缓存的支持

<settings>
    ...
    <!--
            因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。
            为 true 代表开启二级缓存;为 false 代表不开启二级缓存。
        -->
    <setting name="cacheEnabled" value="true"/>
</settings>

在 Mapper 接口中使用注解配置二级缓存

@CacheNamespace
public interface UserMapper {
    ...   
}

测试代码

@Test
public void cacheTest(){
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    User user1 = userMapper1.findById();
    System.out.println(user1);
    // 关闭 sqlSession, 将内容从一级缓存刷新到二级缓存
    sqlSession1.close();

    User user2 = userMapper2.findById();
    System.out.println(user2);
    sqlSession2.close();
}
注解延迟加载

不管是一对一还是一对多 ,在注解配置中都有 fetchType 的属性

  • fetchType = FetchType.LAZY 表示懒加载
  • fetchType = FetchType.EAGER 表示立即加载
  • fetchType = FetchType.DEFAULT 表示使用全局配置
小结

注解开发和 XML 配置优劣分析

  1. 注解开发开发效率更高:注解编写和 XML 配置相比更简单。
  2. XML 维护性更强:注解如果要修改,必须修改源码,会导致维护成本增加。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Renda 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MyBatis 加载策略
    • 什么是延迟加载?
      • 实现
        • 局部延迟加载
        • 设置触发延迟加载的方法
        • 全局延迟加载
    • MyBatis 缓存
      • 为什么使用缓存?
        • 一级缓存
          • 介绍
          • 验证
          • 分析
          • 清除
        • 二级缓存
          • 介绍
          • 验证
          • 分析
          • 小结
      • MyBatis 注解
        • MyBatis 常用注解
          • MyBatis注解的增删改查
            • 创建 `UserMapper` 接口
            • 编写核心配置文件
            • 测试代码
          • 使用注解实现复杂映射开发
            • 一对一查询
              • 介绍
              • 代码实现
            • 一对多查询
              • 介绍
              • 代码实现
            • 多对多查询
              • 介绍
              • 代码实现
            • 基于注解的二级缓存
              • 注解延迟加载
                • 小结
                相关产品与服务
                数据库
                云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档