前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring依赖注入之自动注入

Spring依赖注入之自动注入

作者头像
可为编程
发布2024-02-22 14:53:09
1130
发布2024-02-22 14:53:09
举报

概述

在说自动注入之前呢,先来回顾一下手动注入有哪几种方式?

spring手动注入有三种:

一种是通过xml配置文件的方式,这种方式又分为:

1、set方法进行注入,声明对应的bean标签和属性值。

2、基于构造函数进行注入,通过调用bean所属类的带参构造器为bean的属性注入值。

一种是通过实现factorybean接口进行bean的注入,还可以通过实现一些api接口,例如ImportSelector、ImportBeanDefinitionRegistrar、BeanDefinitionRegistryPostProcessor等实现依赖注入。

一种是通过静态工厂注入和实例工厂注入

自动注入又是怎么一回事呢?

关注公众号【可为编程】回复【加群】进群交流学习!!!

自动注入

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

说之前我们就不得不说一下自动注入这里说的是采用Xml或者是注解形式,都是基于Spring来说的,到后面SpringBoot会对自动注入有所封装,其实本质上都是一样的,都是采用注解形式进行自动注入,只有充分了解了Spring的自动注入我们在后面的学习SpringBoot环节中才会更加透彻和深入的理解自动注入。

一般我们操作自动注入都是采用注解的形式进行注入,其本质上采用的反射机制,通过spring为我们提供的注解加在对应的类名上,方法名上,字段名上就可以由spring帮我们进行自动注入我们想要调用的实例对象。

手动注入的不足:

1、在我们最开始刚学习spring初期,使用xml文件形式配置bean时,如果需要依赖其他或者更多的bean时,需要在xml中配置大量的代码,耦合性太大

2、如果删除某些依赖,就需要再次改动xml文件里的依赖关系,不利于扩展和维护

接下来我们就讲一下自动注入。

自动注入是spring为我们提供的十分方便的注入方式,不需要我们改动过多的代码,遵循约定大于配置的方式,程序与spring之间按照某种规则进行自动注入,注入权交给了spring帮我们实现。

那么是哪种规则呢?

xml中可以在bean元素中通过autowire属性来设置自动注入的方式:

代码语言:javascript
复制
<bean id="" class="" autowire="byType|byName|constructor|default" />
  • byteName:按照名称进行注入
  • byType:按类型进行注入
  • constructor:按照构造方法进行注入
  • default:默认注入方式

下面我们对每种注入方式的用法做一个简单的介绍。

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

按照名称进行注入(byName)

用法

autowire设置为byName

代码语言:javascript
复制
<bean id="" class="X类" autowire="byName"/>

spring容器会按照set属性的名称去容器中查找同名的bean对象,注意这里查找同名的bean是指的根据已知的JavaBeans规范和Spring的工作原理,对于setter方法,如setUser(),Spring会将其解析为属性名“user”,并在容器中寻找与此匹配的bean。因此,答案是它会查找名为“user”的bean,而不是“User”。 然后将查找到的对象通过set方法注入到对应的bean中,未找到对应名称的bean对象则set方法不进行注入。

需要注入的set属性的名称和被注入的bean的名称必须一致。

来看看案例吧。

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
    <!--这些类交给Spring容器进行实例化,然后后面我们根据名称注入这些容器到DiAutowireByName类中-->
    <bean id="service1" class="com.kewei.pojo.DiAutowireByName$Service1">
        <property name="desc" value="service1"/>
    </bean>
    <bean id="service2" class="com.kewei.pojo.DiAutowireByName$Service2">
        <property name="desc" value="service2"/>
    </bean>
    <bean id="service2-1" class="com.kewei.pojo.DiAutowireByName$Service2">
        <property name="desc" value="service2-1"/>
    </bean>

    <bean id="diAutowireByName1" class="com.kewei.pojo.DiAutowireByName" autowire="byName"/>
    <bean id="diAutowireByName2" class="com.kewei.pojo.DiAutowireByName" autowire="byName">
        <property name="service2" ref="service2-1"/>
    </bean>
</beans>

这个类中有2个属性,名称为:

  • service1
  • service2

这两个属性都有对应的set方法。

下面我们在bean.xml中定义2个和这2个属性同名的bean,然后使用按照名称进行自动注入。

测试用例
代码语言:javascript
复制
ClassPathXmlApplicationContext cpaConfig = new ClassPathXmlApplicationContext("keweiAutowireByName.xml");
System.out.println(cpaConfig.getBean("diAutowireByName1"));
System.out.println(cpaConfig.getBean("diAutowireByName2"));
效果

运行diAutowireByName输出:

代码语言:javascript
复制
spring容器启动完毕!
代码语言:javascript
复制
setService1->Service1{desc='service1'}
setService2->Service2{desc='service2'}
setService2->Service2{desc='service2-1'}
setService1->Service1{desc='service1'}
DiAutowireByName{service1=Service1{desc='service1'}, service2=Service2{desc='service2'}}
DiAutowireByName{service1=Service1{desc='service1'}, service2=Service2{desc='service2-1'}}

优缺点

按名称进行注入的时候,要求名称和set属性的名称必须同名,相对于硬编码的方式注入,确实节省了不少代码。

关注公众号【可为编程】回复【加群】进群交流学习!!!

按照类型进行自动注入

用法

autowire设置为byType,在Spring框架中,自动注入(auto-wiring)默认是按照byType方式进行的。这意味着Spring会根据变量类型自动匹配和注入相应的Bean。

代码语言:javascript
复制
<bean id="" class="X类" autowire="byType"/>

这里在注入是会扫描X类中所有的set方法,会在容器中查找与set参数类型相同的Bean对象将其通过set方法进行注入

同时此时在按照类型进行自动装配的时候,如果按照类型找到了多个符合条件的Bean系统会报错,后面我们会通过注解@Qualifier(value = "Bean的id")进行设置。

注意:set方法的参数如果是下面的类型或者下面类型的数组的时候,这个set方法会被跳过注入:Object,Boolean,boolean,Byte,byte,Character,char,Double,double,Float,float,Integer,int,Long,Short,shot,Enum,CharSequence,Number,Date,java.time.temporal.Temporal,java.net.URI,java.net.URI,java.util.Locale,java.lang.Class

还和上面的代码相同,只是在Xml文档中增加按照Bean类型注入的配置:

代码语言:javascript
复制
<bean id="diAutowireByType1" class="com.kewei.pojo.DiAutowireByType" autowire="byType"/>

对于按照类型来说不能重复注入两种类型

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

代码语言:javascript
复制
   <bean id="service1" class="com.kewei.pojo.DiAutowireByType$Service1">
        <property name="desc" value="service1"/>
    </bean>
    <bean id="service2" class="com.kewei.pojo.DiAutowireByType$Service2">
        <property name="desc" value="service2"/>
    </bean>
<!--    <bean id="service2-1" class="com.kewei.pojo.DiAutowireByType$Service2">-->
<!--        <property name="desc" value="service2-1"/>-->
<!--    </bean>-->
    <!-- autowire:byType 配置按照set参数类型进行自动注入 -->
    <bean id="diAutowireByType1" class="com.kewei.pojo.DiAutowireByType" autowire="byType"/>
    <bean id="diAutowireByType2" class="com.kewei.pojo.DiAutowireByType" autowire="byType">
        <property name="service2" ref="service2"/>
    </bean>
代码语言:javascript
复制
spring容器启动完毕!
setService1->Service1{desc='service1'}
setService2->Service2{desc='service2'}
setService2->Service2{desc='service2'}
setService1->Service1{desc='service1'}
DiAutowireByType{service1=Service1{desc='service1'}, service2=Service2{desc='service2'}}
DiAutowireByType{service1=Service1{desc='service1'}, service2=Service2{desc='service2'}}

优缺点

相对于手动注入,节省了不少代码,新增或者删除属性,只需要增减对应的set方法就可以了,更容易扩展了。

按照类型进行注入类型匹配的所有bean(重点)

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

按照类型注入还有2中比较牛逼的用法:

  • 一个容器中满足某种类型的bean可以有很多个,将容器中某种类型中的所有bean,通过set方法注入给一个java.util.List<需要注入的Bean的类型或者其父类型或者其接口>对象
  • 将容器中某种类型中的所有bean,通过set方法注入给一个java.util.Map<String,需要注入的Bean的类型或者其父类型或者其接口>对象

来看个案例就懂了。

代码语言:javascript
复制
public class DiAutowireByTypeExtend {
    //定义了一个接口
    public interface IService1 {
    }
    public static class BaseServie {
        private String desc;
        public String getDesc() {
            return desc;
        }
        public void setDesc(String desc) {
            this.desc = desc;
        }
        @Override
        public String toString() {
            return "BaseServie{" +
                    "desc='" + desc + '\'' +
                    '}';
        }
    }
    //Service1实现了IService1接口
    public static class Service1 extends BaseServie implements IService1 {
    }
    //Service1实现了IService1接口
    public static class Service2 extends BaseServie implements IService1 {
    }
    private List<IService1> serviceList;//@1
    private List<BaseServie> baseServieList;//@2
    private Map<String, IService1> service1Map;//@3
    private Map<String, BaseServie> baseServieMap;//@4
    public List<IService1> getServiceList() {
        return serviceList;
    }
    public void setServiceList(List<IService1> serviceList) {//@5
        this.serviceList = serviceList;
    }
    public List<BaseServie> getBaseServieList() {
        return baseServieList;
    }
    public void setBaseServieList(List<BaseServie> baseServieList) {//@6
        this.baseServieList = baseServieList;
    }
    public Map<String, IService1> getService1Map() {
        return service1Map;
    }
    public void setService1Map(Map<String, IService1> service1Map) {//@7
        this.service1Map = service1Map;
    }
    public Map<String, BaseServie> getBaseServieMap() {
        return baseServieMap;
    }
    public void setBaseServieMap(Map<String, BaseServie> baseServieMap) {//@8
        this.baseServieMap = baseServieMap;
    }
    @Override
    public String toString() { //9
        return "DiAutowireByTypeExtend{" +
                "serviceList=" + serviceList +
                ", baseServieList=" + baseServieList +
                ", service1Map=" + service1Map +
                ", baseServieMap=" + baseServieMap +
                '}';
    }
}

@1,@2,@3,@4:定义了4个属性,都是泛型类型的,都有对应的set方法。

@5:参数类型是List<BaseServie>,这个集合集合中元素的类型是BaseServie,spring会找到容器中所有满足BaseServie.isAssignableFrom(bean的类型)的bean列表,将其通过@5的set方法进行注入。

@6:同@5的代码

@7:这个参数类型是一个map了,map的key是string类型,value是IService1类型,spring容器会将所有满足IService1类型的bean找到,按照name->bean对象这种方式丢到一个map中,然后调用@7的set方法进行注入,最后注入的这个map就是bean的名称和bean对象进行映射的一个map对象。

@8:同@7的代码

@9:重写了toString方法,输出的时候好看一些

比如我们给map或list设置值

代码语言:javascript
复制
<!--这些类交给Spring容器进行实例化,然后后面我们根据名称注入这些容器到DiAutowireByName类中-->
<bean id="service1" class="com.kewei.pojo.DiAutowireByTypeExtend$Service1">
    <property name="desc" value="service1"/>
</bean>
<bean id="service2" class="com.kewei.pojo.DiAutowireByTypeExtend$Service2">
    <property name="desc" value="service2"/>
</bean>
<bean id="baseServie" class="com.kewei.pojo.DiAutowireByTypeExtend$BaseServie">
    <property name="desc" value="baseServie"/>
</bean>

<bean id="keweiAutowireByName1" class="com.kewei.pojo.DiAutowireByTypeExtend">
    <property name="serviceList">
        <list>
            <ref bean="service1"/>
            <ref bean="service2"/>
        </list>
    </property>
    <property name="service1Map">
        <map>
            <entry key="service1" value-ref="service1"/>
        </map>
    </property>
</bean>

输出结果:

代码语言:javascript
复制
spring容器启动完毕!
DiAutowireByTypeExtend{serviceList=[BaseServie{desc='service1'}, BaseServie{desc='service2'}], baseServieList=null, service1Map={service1=BaseServie{desc='service1'}}, baseServieMap=null}

按照构造函数进行自动注入

用法

autowire设置为constructor

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

代码语言:javascript
复制
<bean id="" class="X类" autowire="constructor"/>

spring会找到x类中所有的构造方法(一个类可能有多个构造方法),然后将这些构造方法进行排序(先按修饰符进行排序,public的在前面,其他的在后面,如果修饰符一样的,会按照构造函数参数数量倒序,也就是采用贪婪的模式进行匹配,spring容器会尽量多注入一些需要的对象)得到一个构造函数列表,会轮询这个构造器列表,判断当前构造器所有参数是否在容器中都可以找到匹配的bean对象,如果可以找到就使用这个构造器进行注入,如果不能找到,那么就会跳过这个构造器,继续采用同样的方式匹配下一个构造器,直到找到一个合适的为止。

案例

来看看案例,这里我们创建三种不同的构造函数

代码语言:javascript
复制
 public DiAutowireByConstructor() { //@0
    }
    public DiAutowireByConstructor(Service1 service1) { //@1
        System.out.println("DiAutowireByConstructor(Service1 service1)");
        this.service1 = service1;
    }
    public DiAutowireByConstructor(Service1 service1, Service2 service2) { //@2
        System.out.println("DiAutowireByConstructor(Service1 service1, Service2 service2)");
        this.service1 = service1;
        this.service2 = service2;
    }

@1:1个参数的构造函数

@2:2个参数的构造函数

2个有参构造函数第一行都打印了一段文字,一会在输出中可以看到代码是调用了那个构造函数创建对象。autowire="constructor"

代码语言:javascript
复制
<bean id="service1" class="com.kewei.pojo.DiAutowireByType$Service1">
    <property name="desc" value="service1"/>
</bean>
<bean id="diAutowireByType1" class="com.kewei.pojo.DiAutowireByType" autowire="constructor"/>

执行结果:

代码语言:javascript
复制
spring容器启动完毕!
DiAutowireByConstructor(Service1 service1)
DiAutowireByType{service1=Service1{desc='service1'}, service2=null

从输出中可以看到调用的是DiAutowireByConstructor类中的第一个构造函数注入了service1 bean。

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

构造函数匹配采用贪婪匹配,多个构造函数结合容器找到一个合适的构造函数,最匹配的就是第一个有参构造函数,而第二个有参构造函数的第二个参数在spring容器中找不到匹配的bean对象,所以被跳过了。

我们在diAutowireByConstructor.xml加入Service2的配置:

代码语言:javascript
复制
<bean id="service2" class="com.kewei.pojo.DiAutowireByType$Service2">
    <property name="desc" value="service2"/>
</bean>

再来运行一下diAutowireByConstructor输出:

代码语言:javascript
复制
spring容器启动完毕!
DiAutowireByConstructor(Service1 service1 Service2 service2)
DiAutowireByType{service1=Service1{desc='service1'}, service2=Service2{desc='service2'}}

此时可以看到第二个有参构造函数被调用了,满足了贪婪方式的注入原则,最大限度的注入所有依赖的对象。

基于注解的构造函数进行自动注入

我们实际开发过程中,一般都采用@Autowired进行属性注入或者是采用构造函数进行属性注入,阿里编码规范中推荐我们采用构造函数进行属性注入,我们来看以下代码:

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

代码语言:javascript
复制

private final DetailsHaulTripsService detailsHaulTripsService;
public DataBoardController(DetailsHaulTripsService detailsHaulTripsService) {
    this.detailsHaulTripsService = detailsHaulTripsService;
}

这里我们采用了DataBoardController的有参构造函数对DetailsHaulTripsService进行注入,我们也可以采用@Autowired注解进行注入,但是我们一般会分层去写代码,比如Controller、Service、ServiceImpl、Dao、Entity等层次,这也是传统的MVC思想。

代码语言:javascript
复制
public interface DetailsHaulTripsService {
    /**
     * 新增
     * @param detailsHaulTripsDTO
     * @return
     */
    ApiResultUtil saveDetailsHaulTrips(DetailsHaulTripsDTO detailsHaulTripsDTO);
}
代码语言:javascript
复制
@Service
public class DetailsHaulTripsServiceImpl implements DetailsHaulTripsService {

    public ApiResultUtil saveDetailsHaulTrips(DetailsHaulTripsDTO detailsHaulTripsDTO) {
        return null;
    }
}

但是有没有发现,我们将实际的业务逻辑写在了ServiceImpl类中,为什么我们没有在DataBoardController中采用DetailsHaulTripsServiceImpl 类而是采用DetailsHaulTripsService 类了呢?

这就是Spring注解的魅力,Spring将@Service注解加在DetailsHaulTripsServiceImpl实现类上,当容器启动的时候就会去扫描所有带有容器类注解标注的类并对其进行实例化并注册到容器中,又因为DetailsHaulTripsServiceImpl实现了DetailsHaulTripsService 因此我们在DataBoardController中注入DetailsHaulTripsService对象的时候,会自动扫描DetailsHaulTripsService 接口的实现类,因为在Spring中只有一个实现类就是detailsHaulTripsServiceImpl (注意这里是小写的名称,本质是通过set方法进行注入),所以在我们调用的时候能够顺利调用到我们编写的业务逻辑代码。

如果存在容器中存在两个detailsHaulTripsServiceImpl 对象怎么办呢?

我们可以采用这种方式,对于这些注解我在后面会详细的讲解怎么使用,能做什么。

代码语言:javascript
复制
代码语言:javascript
复制
@Qualifier("detailsHaulTripsServiceImpl")
@Autowired
private  DetailsHaulTripsService detailsHaulTripsService;

或者是采用在那个bean的定义上加上@Primary注解。这样,在没有明确指定@Qualifier的情况下,Spring会优先选择带有@Primary注解的bean进行注入。

当我们输入一个不存在的Bean时,就会报错

代码语言:javascript
复制
@Qualifier("detailsHaulTripsServiceImpl1")
@Autowired
private  DetailsHaulTripsService detailsHaulTripsService;
代码语言:javascript
复制
No qualifying bean of type 'com.itage.maintain.service.DetailsHaulTripsService'
available: expected at least 1 bean which qualifies as autowire candidate.
Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=detailsHaulTripsServiceImpl1),
@org.springframework.beans.factory.annotation.Autowired(required=true)}

无论@Service注解是在接口上还是在实现类上,Spring都会将实现类作为Bean进行实例化和管理。在注入时,Spring会根据需要注入的类型来查找对应的Bean实例。由于Java支持接口的多态性,所以即使注入的是接口类型,实际上注入的也可以是这个接口的实现类的实例。

按照默认Default进行自动注入

用法

bean xml的根元素为beans,注意根元素有个default-autowire属性,这个属性可选值有(no|byName|byType|constructor|default)。

关注公众号【可为编程】回复【面试】领取年度最新面试题大全!!!

这个属性可以批量设置当前文件中所有bean的自动注入的方式,bean元素中如果省略了autowire属性,那么会取default-autowire的值作为其autowire的值,而每个bean元素还可以单独设置自己的autowire覆盖default-autowire的配置,如下:

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd"
       default-autowire="byName">

</beans>

案例

diAutowireByDefault.xml
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd"
       default-autowire="byName"> //@1

    <bean id="service1" class="com.javacode2018.lesson001.demo6.DiAutowireByName$Service1">
        <property name="desc" value="service1"/>
    </bean>
    <bean id="service2" class="com.javacode2018.lesson001.demo6.DiAutowireByName$Service2">
        <property name="desc" value="service2"/>
    </bean>
    <bean id="service2-1" class="com.javacode2018.lesson001.demo6.DiAutowireByName$Service2">
        <property name="desc" value="service2-1"/>
    </bean>
    <!-- autowire:default,会采用beans中的default-autowire指定的配置 -->
    <bean id="diAutowireByDefault1" class="com.javacode2018.lesson001.demo6.DiAutowireByName" autowire="default"/> //@2


    <!-- autowire:default,会采用beans中的default-autowire指定的配置,还可以使用手动的方式自动注入进行覆盖,手动的优先级更高一些 -->
    <bean id="diAutowireByDefault2" class="com.javacode2018.lesson001.demo6.DiAutowireByName" autowire="default"> //@3
        <property name="service2" ref="service2-1"/>
    </bean>

</beans>

注意上面的@1配置的default-autowire=”byName”,表示全局默认的自动注入方式是:按名称注入

@2和@3的autowire=default,那么注入方式会取default-autowire的值。

测试用例

DiAutowireTest中新增一个方法

代码语言:javascript
复制
/**
 * autowire=default
 */
@Test
public void diAutowireByDefault() {
    String beanXml = "classpath:/com/javacode2018/lesson001/demo6/diAutowireByDefault.xml";
    ClassPathXmlApplicationContext context = IocUtils.context(beanXml);
    System.out.println(context.getBean("diAutowireByDefault1"));
    System.out.println(context.getBean("diAutowireByDefault2"));
}
效果

运行diAutowireByDefault输出

代码语言:javascript
复制
setService1->Service1{desc='service1'}
setService2->Service2{desc='service2'}
setService2->Service2{desc='service2-1'}
setService1->Service1{desc='service1'}
DiAutowireByName{service1=Service1{desc='service1'}, service2=Service2{desc='service2'}}
DiAutowireByName{service1=Service1{desc='service1'}, service2=Service2{desc='service2-1'}}

总结

  • xml中手动注入存在的不足之处,可以通过自动注入的方式来解决,本文介绍了3中自动注入:通过名称自动注入、通过类型自动注入、通过构造器自动注入
  • 在按类型注入中还有个比较重要的是注入匹配类型所有的bean,可以将某种类型所有的bean注入给一个List对象,可以将某种类型的所有bean按照bean名称->bean对象的映射方式注入给一个Map对象,这种用法比较重要,用途比较大
  • spring中还有其他自动注入的方式,用起来会更爽,后面的文章中我们会详细介绍。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-02-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 可为编程 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 用法
    • 测试用例
      • 效果
      • 优缺点
      • 用法
      • 优缺点
      • 按照类型注入还有2中比较牛逼的用法:
      • 用法
      • 案例
      • 用法
      • 案例
        • diAutowireByDefault.xml
          • 测试用例
            • 效果
            • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档